/*
 * Cppcheck - A tool for static C/C++ code analysis
 * Copyright (C) 2007-2019 Cppcheck team.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "suppressions.h"

#include "errorlogger.h"
#include "mathlib.h"
#include "path.h"

#include <tinyxml2.h>

#include <algorithm>
#include <cctype>   // std::isdigit, std::isalnum, etc
#include <stack>
#include <sstream>
#include <utility>

class ErrorLogger;

static bool isValidGlobPattern(const std::string &pattern)
{
    for (std::string::const_iterator i = pattern.begin(); i != pattern.end(); ++i) {
        if (*i == '*' || *i == '?') {
            std::string::const_iterator j = i + 1;
            if (j != pattern.end() && (*j == '*' || *j == '?')) {
                return false;
            }
        }
    }
    return true;
}

static bool isAcceptedErrorIdChar(char c)
{
    switch (c) {
    case '_':
    case '-':
    case '.':
        return true;
    default:
        return std::isalnum(c);
    }
}

std::string Suppressions::parseFile(std::istream &istr)
{
    // Change '\r' to '\n' in the istr
    std::string filedata;
    std::string line;
    while (std::getline(istr, line))
        filedata += line + "\n";
    std::replace(filedata.begin(), filedata.end(), '\r', '\n');

    // Parse filedata..
    std::istringstream istr2(filedata);
    while (std::getline(istr2, line)) {
        // Skip empty lines
        if (line.empty())
            continue;

        // Skip comments
        if (line.length() > 1 && line[0] == '#')
            continue;
        if (line.length() >= 2 && line[0] == '/' && line[1] == '/')
            continue;

        const std::string errmsg(addSuppressionLine(line));
        if (!errmsg.empty())
            return errmsg;
    }

    return "";
}


std::string Suppressions::parseXmlFile(const char *filename)
{
    tinyxml2::XMLDocument doc;
    const tinyxml2::XMLError error = doc.LoadFile(filename);
    if (error == tinyxml2::XML_ERROR_FILE_NOT_FOUND)
        return "File not found";
    if (error != tinyxml2::XML_SUCCESS)
        return "Failed to parse XML file";

    const tinyxml2::XMLElement * const rootnode = doc.FirstChildElement();
    for (const tinyxml2::XMLElement * e = rootnode->FirstChildElement(); e; e = e->NextSiblingElement()) {
        if (std::strcmp(e->Name(), "suppress") != 0)
            return "Invalid suppression xml file format, expected <suppress> element but got a <" + std::string(e->Name()) + '>';

        Suppression s;
        for (const tinyxml2::XMLElement * e2 = e->FirstChildElement(); e2; e2 = e2->NextSiblingElement()) {
            const char *text = e2->GetText() ? e2->GetText() : "";
            if (std::strcmp(e2->Name(), "id") == 0)
                s.errorId = text;
            else if (std::strcmp(e2->Name(), "fileName") == 0)
                s.fileName = text;
            else if (std::strcmp(e2->Name(), "lineNumber") == 0)
                s.lineNumber = std::atoi(text);
            else if (std::strcmp(e2->Name(), "symbolName") == 0)
                s.symbolName = text;
            else
                return "Unknown suppression element <" + std::string(e2->Name()) + ">, expected <id>/<fileName>/<lineNumber>/<symbolName>";
        }

        const std::string err = addSuppression(s);
        if (!err.empty())
            return err;
    }

    return "";
}

std::string Suppressions::addSuppressionLine(const std::string &line)
{
    std::istringstream lineStream(line);
    Suppressions::Suppression suppression;
    if (std::getline(lineStream, suppression.errorId, ':')) {
        if (std::getline(lineStream, suppression.fileName)) {
            // If there is not a dot after the last colon in "file" then
            // the colon is a separator and the contents after the colon
            // is a line number..

            // Get position of last colon
            const std::string::size_type pos = suppression.fileName.rfind(':');

            // if a colon is found and there is no dot after it..
            if (pos != std::string::npos &&
                suppression.fileName.find('.', pos) == std::string::npos) {
                // Try to parse out the line number
                try {
                    std::istringstream istr1(suppression.fileName.substr(pos+1));
                    istr1 >> suppression.lineNumber;
                } catch (...) {
                    suppression.lineNumber = Suppressions::Suppression::NO_LINE;
                }

                if (suppression.lineNumber != Suppressions::Suppression::NO_LINE) {
                    suppression.fileName.erase(pos);
                }
            }
        }
    }

    suppression.fileName = Path::simplifyPath(suppression.fileName);

    return addSuppression(suppression);
}

std::string Suppressions::addSuppression(const Suppressions::Suppression &suppression)
{
    // Check that errorId is valid..
    if (suppression.errorId.empty()) {
        return "Failed to add suppression. No id.";
    }
    if (suppression.errorId != "*") {
        for (std::string::size_type pos = 0; pos < suppression.errorId.length(); ++pos) {
            if (suppression.errorId[pos] < 0 || !isAcceptedErrorIdChar(suppression.errorId[pos])) {
                return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\"";
            }
            if (pos == 0 && std::isdigit(suppression.errorId[pos])) {
                return "Failed to add suppression. Invalid id \"" + suppression.errorId + "\"";
            }
        }
    }

    if (!isValidGlobPattern(suppression.errorId))
        return "Failed to add suppression. Invalid glob pattern '" + suppression.errorId + "'.";
    if (!isValidGlobPattern(suppression.fileName))
        return "Failed to add suppression. Invalid glob pattern '" + suppression.fileName + "'.";

    mSuppressions.push_back(suppression);

    return "";
}

void Suppressions::ErrorMessage::setFileName(const std::string &s)
{
    mFileName = Path::simplifyPath(s);
}

bool Suppressions::Suppression::parseComment(std::string comment, std::string *errorMessage)
{
    if (comment.size() < 2)
        return false;

    if (comment.find(';') != std::string::npos)
        comment.erase(comment.find(';'));

    if (comment.find("//", 2) != std::string::npos)
        comment.erase(comment.find("//",2));

    if (comment.compare(comment.size() - 2, 2, "*/") == 0)
        comment.erase(comment.size() - 2, 2);

    std::istringstream iss(comment.substr(2));
    std::string word;
    iss >> word;
    if (word != "cppcheck-suppress")
        return false;
    iss >> errorId;
    if (!iss)
        return false;
    while (iss) {
        iss >> word;
        if (!iss)
            break;
        if (word.find_first_not_of("+-*/%#;") == std::string::npos)
            break;
        if (word.compare(0,11,"symbolName=")==0)
            symbolName = word.substr(11);
        else if (errorMessage && errorMessage->empty())
            *errorMessage = "Bad suppression attribute '" + word + "'. You can write comments in the comment after a ; or //. Valid suppression attributes; symbolName=sym";
    }
    return true;
}

bool Suppressions::Suppression::isSuppressed(const Suppressions::ErrorMessage &errmsg) const
{
    if (!errorId.empty() && !matchglob(errorId, errmsg.errorId))
        return false;
    if (!fileName.empty() && !matchglob(fileName, errmsg.getFileName()))
        return false;
    if (lineNumber != NO_LINE && lineNumber != errmsg.lineNumber)
        return false;
    if (!symbolName.empty()) {
        for (std::string::size_type pos = 0; pos < errmsg.symbolNames.size();) {
            const std::string::size_type pos2 = errmsg.symbolNames.find('\n',pos);
            std::string symname;
            if (pos2 == std::string::npos) {
                symname = errmsg.symbolNames.substr(pos);
                pos = pos2;
            } else {
                symname = errmsg.symbolNames.substr(pos,pos2-pos);
                pos = pos2+1;
            }
            if (matchglob(symbolName, symname))
                return true;
        }
        return false;
    }
    return true;
}

bool Suppressions::Suppression::isMatch(const Suppressions::ErrorMessage &errmsg)
{
    if (!isSuppressed(errmsg))
        return false;
    matched = true;
    return true;
}

std::string Suppressions::Suppression::getText() const
{
    std::string ret;
    if (!errorId.empty())
        ret = errorId;
    if (!fileName.empty())
        ret += " fileName=" + fileName;
    if (lineNumber != NO_LINE)
        ret += " lineNumber=" + MathLib::toString(lineNumber);
    if (!symbolName.empty())
        ret += " symbolName=" + symbolName;
    if (ret.compare(0,1," ")==0)
        return ret.substr(1);
    return ret;
}

bool Suppressions::isSuppressed(const Suppressions::ErrorMessage &errmsg)
{
    const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression");
    for (Suppression &s : mSuppressions) {
        if (unmatchedSuppression && s.errorId != errmsg.errorId)
            continue;
        if (s.isMatch(errmsg))
            return true;
    }
    return false;
}

bool Suppressions::isSuppressedLocal(const Suppressions::ErrorMessage &errmsg)
{
    const bool unmatchedSuppression(errmsg.errorId == "unmatchedSuppression");
    for (Suppression &s : mSuppressions) {
        if (!s.isLocal())
            continue;
        if (unmatchedSuppression && s.errorId != errmsg.errorId)
            continue;
        if (s.isMatch(errmsg))
            return true;
    }
    return false;
}

void Suppressions::dump(std::ostream & out) const
{
    out << "  <suppressions>" << std::endl;
    for (const Suppression &suppression : mSuppressions) {
        out << "    <suppression";
        out << " errorId=\"" << ErrorLogger::toxml(suppression.errorId) << '"';
        if (!suppression.fileName.empty())
            out << " fileName=\"" << ErrorLogger::toxml(suppression.fileName) << '"';
        if (suppression.lineNumber != Suppression::NO_LINE)
            out << " lineNumber=\"" << suppression.lineNumber << '"';
        if (!suppression.symbolName.empty())
            out << " symbolName=\"" << ErrorLogger::toxml(suppression.symbolName) << '\"';
        out << " />" << std::endl;
    }
    out << "  </suppressions>" << std::endl;
}

#include <iostream>

std::list<Suppressions::Suppression> Suppressions::getUnmatchedLocalSuppressions(const std::string &file, const bool unusedFunctionChecking) const
{
    std::list<Suppression> result;
    for (const Suppression &s : mSuppressions) {
        if (s.matched)
            continue;
        if (!unusedFunctionChecking && s.errorId == "unusedFunction")
            continue;
        if (file.empty() || !s.isLocal() || s.fileName != file)
            continue;
        result.push_back(s);
    }
    return result;
}

std::list<Suppressions::Suppression> Suppressions::getUnmatchedGlobalSuppressions(const bool unusedFunctionChecking) const
{
    std::list<Suppression> result;
    for (const Suppression &s : mSuppressions) {
        if (s.matched)
            continue;
        if (!unusedFunctionChecking && s.errorId == "unusedFunction")
            continue;
        if (s.isLocal())
            continue;
        result.push_back(s);
    }
    return result;
}

bool Suppressions::matchglob(const std::string &pattern, const std::string &name)
{
    const char *p = pattern.c_str();
    const char *n = name.c_str();
    std::stack<std::pair<const char *, const char *> > backtrack;

    for (;;) {
        bool matching = true;
        while (*p != '\0' && matching) {
            switch (*p) {
            case '*':
                // Step forward until we match the next character after *
                while (*n != '\0' && *n != p[1]) {
                    n++;
                }
                if (*n != '\0') {
                    // If this isn't the last possibility, save it for later
                    backtrack.push(std::make_pair(p, n));
                }
                break;
            case '?':
                // Any character matches unless we're at the end of the name
                if (*n != '\0') {
                    n++;
                } else {
                    matching = false;
                }
                break;
            default:
                // Non-wildcard characters match literally
                if (*n == *p) {
                    n++;
                } else if (*n == '\\' && *p == '/') {
                    n++;
                } else if (*n == '/' && *p == '\\') {
                    n++;
                } else {
                    matching = false;
                }
                break;
            }
            p++;
        }

        // If we haven't failed matching and we've reached the end of the name, then success
        if (matching && *n == '\0') {
            return true;
        }

        // If there are no other paths to try, then fail
        if (backtrack.empty()) {
            return false;
        }

        // Restore pointers from backtrack stack
        p = backtrack.top().first;
        n = backtrack.top().second;
        backtrack.pop();

        // Advance name pointer by one because the current position didn't work
        n++;
    }
}
