using namespace std;

void version()
{
    cout << "ipmt version 0.1" << endl
         << "Developed and maintained by" << endl
         << "* Miguel Araújo <miguelarauj1o@gmail.com>" << endl
         << "* Paulo Lieuthier <paulolieuthier@gmail.com>" << endl;
}

void help(const string &programName)
{
    // clang-format off
    cout << "Indexed Pattern Matching Tool is a mechanism for indexing," << endl
         << "storing and searching patterns in a text file." << endl
         << "Usage: " << programName << " [options] [PATTERN] FILE" << endl
         << "\t" << "-p, --pattern PATTERN_FILE" << "\t"
         << "Search patterns from PATTERN_FILE" << endl
         << "\t" << "--compression=ALGORITHM" << "\t\t"
         << "Select a compression algorithm" << endl
         << "\t\t\t\t\t\t" << "* HUFFMAN (Default)" << endl
         << "\t\t\t\t\t\t" << "* LZW" << endl
         << "\t\t\t\t\t\t" << "* LZ77" << endl
         << "\t" << "-c, --count" << "\t\t\t" << "Count matches" << endl
         << "\t" << "-h, --help" << "\t\t\t" << "Print this output" << endl
         << "\t" << "-v, --version" << "\t\t\t" << "Print version information"
         << endl;
    // clang-format on
}

void die(const string &error)
{
    cout << error << "." << endl
         << "Use the --help flag for usage and options." << endl;
    exit(1);
}

int main(int argc, char const *argv[])
{
    // disable io sync for faster output
    ios::sync_with_stdio(false);

    deque<string> positionalArguments;

    // -p flag
    string patternFile;

    // -c flag
    bool count = false;

    // compression flag
    CompressionType compressionType = CompressionType::HUFFMAN;

    string arg;
    for (int i = 1; i < argc; i++)
    {
        arg = argv[i];
        if (arg == "-v" || arg == "--version")
        {
            version();
            return 0;
        }
        else if (arg == "-h" || arg == "--help")
        {
            help(argv[0]);
            return 0;
        }
        else if (arg == "-c" || arg == "--count")
            count = true;
        else if (arg.compare(0, 13, "--compression") == 0)
        {
            // if no text after '--compression'
            if (arg.size() < 15)
                die("Missing compression type");

            string compressionTypeStr = arg.substr(14);
            transform(compressionTypeStr.begin(), compressionTypeStr.end(),
                      compressionTypeStr.begin(), ::tolower);

            if (compressionTypeStr == "huffman")
                compressionType = CompressionType::HUFFMAN;
            else if (compressionTypeStr == "lz77")
                compressionType = CompressionType::LZ77;
            else
                compressionType = CompressionType::LZW;
        }
        else if (arg == "-p" || arg == "--pattern")
        {
            if (++i >= argc)
                die("Missing PATTERN_FILE after -p");

            patternFile = argv[i];
            if (!ifstream(patternFile).good())
                die("Pattern file not accessible");
        }
        else if (arg[0] == '-')
            die("Invalid argument " + arg);
        else
            positionalArguments.push_back(argv[i]);
    }

    if (positionalArguments.empty())
        die("Not enough arguments");

    // enough of arguments handling, let's get to work

    const string operation = positionalArguments.front();
    positionalArguments.pop_front();

    if (operation == "index")
    {
        if (positionalArguments.size() != 1)
            die("Text file after 'index' expected");

        // test file
        const string file = positionalArguments.front();
        positionalArguments.pop_front();
        ifstream fileStream(file);
        if (!fileStream.good())
            die("Input file \"" + file + "\" not accessible");

        ByteArray compressedData;

        // use scopes to free unnecessary memory
        {
            // read from file
            string content;
            fileStream.seekg(0, ios::end);
            int lol = fileStream.tellg();
            content.reserve(lol);
            fileStream.seekg(0, ios::beg);
            content.assign((istreambuf_iterator<char>(fileStream)),
                           istreambuf_iterator<char>());

            vector<int> sa = SuffixArray(content).array();
            int saLength = sa.size();
            int saBytes = saLength * sizeof(int);

            // insert suffix array
            content.insert(0, reinterpret_cast<char *>(&*sa.begin()), saBytes);
            // append array size
            content.insert(0, reinterpret_cast<char *>(&saLength), sizeof(int));

            // compress
            compressedData = encode(compressionType, content);
        }

        int compressedBytes
            = compressedData.size() * sizeof(ByteArray::value_type);

        // get output file name
        string fileName = file;
        const size_t slash = fileName.find_last_of("\\/");
        if (slash != string::npos)
            fileName = fileName.substr(slash + 1);
        const size_t ext = fileName.rfind(".txt");
        if (ext != string::npos)
            fileName.erase(ext);

        // write file
        ofstream out(fileName + ".idx", ios::binary);
        out.write(&*compressedData.begin(), compressedBytes);
        out.close();
    }
    else if (operation == "search")
    {
        string pattern;

        if (positionalArguments.empty())
            die("Argument after 'search' expected");

        // if there is no pattern file, expect two arguments
        if (patternFile.empty() && positionalArguments.size() != 2)
            die("Not enough arguments");
        // if there is pattern file, take pattern from positional arguments
        else if (patternFile.empty())
        {
            pattern = positionalArguments.front();
            positionalArguments.pop_front();
        }

        const string indexfile = positionalArguments.front();
        positionalArguments.pop_front();

        // get file size
        ifstream indexStream(indexfile, ios::binary);
        if (!indexStream.good())
            die("Index file not accessible");
        indexStream.seekg(0, ios::end);
        int compressedBytes = indexStream.tellg();
        indexStream.seekg(0, ios::beg);

        string text;
        vector<int> suffixArray;

        // use scopes to free unnecessary memory
        {
            string allData;

            {
                // read file
                ByteArray compressed;
                compressed.reserve(compressedBytes);
                compressed.assign((istreambuf_iterator<char>(indexStream)),
                                  istreambuf_iterator<char>());

                // decompress
                allData = decode(compressionType, compressed);
            }

            istringstream iss(allData, ios::binary);

            // get suffix array length
            int saLength;
            iss.read(reinterpret_cast<char *>(&saLength), sizeof(int));
            // get suffix array
            suffixArray = vector<int>(saLength);
            iss.read(reinterpret_cast<char *>(&*suffixArray.begin()),
                     saLength * sizeof(int));
            // get text
            text.reserve(allData.size() - sizeof(int) * (1 + saLength));
            text.assign((istreambuf_iterator<char>(iss)),
                        istreambuf_iterator<char>());
        }

        // search
        function<void(const string &) > lambda;
        int counter = 0;
        if (count)
            lambda = [&counter](const string &)
            {
                ++counter;
            };
        else
            lambda = [](const string &line)
            {
                cout << line << endl;
            };

        if (patternFile.empty())
            search({pattern}, text, suffixArray, lambda);
        else
        {
            vector<string> patterns;
            ifstream pFile(patternFile);
            for (string line; getline(pFile, line);)
                patterns.push_back(line);
            search(patterns, text, suffixArray, lambda);
        }

        if (count)
            printf("%d\n", counter);
    }
    else
        die("You can only index or search");

    return 0;
}
