/** @file
 * @brief OmegaScript $sort function
 */
/* Copyright (C) 2001,2004,2012,2016,2018 Olly Betts
 *
 * 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 2 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
 * <https://www.gnu.org/licenses/>.
 */

#include <config.h>

#include "sort.h"

// For option["decimal"].
#include "omega.h"
#include "stringutils.h"

#include <algorithm>
#include <string>
#include <vector>

using namespace std;

// Return -1, 0 or 1 depending on sign of floating point number starting at
// index i of string s.  Non-numbers give 0.
static int
string_sign(const string& s, const string& decimal, size_t i = 0)
{
    int r = 1;
    if (s[i] == '-') {
        r = -1;
        ++i;
    }
    while (s[i] == '0') {
        ++i;
    }
    if (s.compare(i, decimal.size(), decimal) == 0) {
        i += decimal.size();
        while (s[i] == '0') {
            ++i;
        }
    }
    return C_isdigit(s[i]) ? r : 0;
}

// Compare strings of two non-zero floating point numbers, without loss of
// precision, and ignoring their signs.
//
// The format handled is (as a Perl regex):
//
// ^[ \t]*(-?[0-9]*($decimal[0-9]*)?)
//
// where $decimal is option["decimal"].
//
// Values which are equal by the numeric test are compared as strings.
static int
ncmp(const string& a, const string& b, const string& decimal)
{
    // Ignore leading blanks, an optional minus sign, and leading zeros.
    size_t ai = a.find_first_not_of(" \t");
    if (a[ai] == '-') ++ai;
    while (a[ai] == '0') ++ai;

    size_t bi = b.find_first_not_of(" \t");
    if (b[bi] == '-') ++bi;
    while (b[bi] == '0') ++bi;

    while (a[ai] == b[bi] && C_isdigit(a[ai])) {
        // Matching digits.
        ++ai;
        ++bi;
    }

    if (C_isdigit(a[ai])) {
        if (!C_isdigit(b[bi])) {
            // a > b
            return 1;
        }

        int r = a[ai] - b[bi];
        bool a_digit, b_digit;
        do {
            ++ai;
            ++bi;
            a_digit = C_isdigit(a[ai]);
            b_digit = C_isdigit(b[bi]);
        } while (a_digit && b_digit);

        if (!a_digit && !b_digit) {
            // Same number of digits, but not the same digits.
            return r;
        }
        return a_digit ? 1 : -1;
    }

    if (C_isdigit(b[bi])) {
        // a < b
        return -1;
    }

    // Non-fractional parts are the same - compare any fractional parts.
    bool a_frac = (a.compare(ai, decimal.size(), decimal) == 0);
    if (a_frac) ai += decimal.size();
    bool b_frac = (b.compare(bi, decimal.size(), decimal) == 0);
    if (b_frac) bi += decimal.size();
    if (!a_frac && !b_frac) return 0;
    if (!b_frac) {
        // Check if a's fractional part is zero.
        while (a[ai] == '0') ++ai;
        return C_isdigit(a[ai]) ? 1 : 0;
    }
    if (!a_frac) {
        // Check if b's fractional part is zero.
        while (b[bi] == '0') ++bi;
        return C_isdigit(b[bi]) ? -1 : 0;
    }

    // Both have fractional parts, so compare.
    while (a[ai] == b[bi] && C_isdigit(a[ai])) {
        ++ai;
        ++bi;
    }
    if (C_isdigit(a[ai])) return 1;
    if (C_isdigit(b[bi])) return -1;
    return 0;
}

static int
natcmp(const string& a, const string& b)
{
    size_t shorter = min(a.size(), b.size());
    size_t i = 0;
    while (i != shorter) {
        int cha = static_cast<unsigned char>(a[i]);
        int chb = static_cast<unsigned char>(b[i]);
        if (!C_isdigit(cha)) {
            if (cha == chb) {
                // Matching non-digits.
                ++i;
                continue;
            }

            if (C_isdigit(chb)) return 1;
            return cha - chb;
        }

        // Sort embedded digit spans by numeric value and before non-digits.
        if (!C_isdigit(chb)) return -1;

        // Skip any leading zeros on each.
        size_t sa = i;
        while (a[sa] == '0') ++sa;
        size_t sb = i;
        while (b[sb] == '0') ++sb;

        size_t ea = sa;
        size_t eb = sb;
        int res = 0;
        while (true) {
            if (!C_isdigit(a[ea])) {
                if (C_isdigit(b[eb])) {
                    // Number in b is longer and so larger.
                    return -1;
                }
                // Digit sequences are the same length.
                break;
            }
            if (a[ea] != b[eb]) {
                if (!C_isdigit(b[eb])) {
                    // Number in a is longer and so larger.
                    return 1;
                }
                // Record first difference between digits.
                if (res == 0) res = a[ea] - b[eb];
            }
            ++ea;
            ++eb;
        }

        if (res == 0) {
            // More leading zeros sorts first.
            res = int(sb) - int(sa);
        }
        if (res) return res;

        // Digit sequences were identical, so keep going.
        i = ea;
    }
    return int(a.size()) - int(b.size());
}

void
omegascript_sort(const vector<string>& args,
                 string& value)
{
    const string &list = args[0];
    if (list.empty()) return;

    // 0 => string sort, '#' => natural, 'n' => numeric.
    char mode = 0;
    bool uniq = false;
    bool rev = false;
    if (args.size() > 1) {
        for (auto opt_ch : args[1]) {
            switch (opt_ch) {
                case '#':
                case 'n':
                    if (mode != 0) {
                        string m = "Invalid $sort option combination: ";
                        m += args[1];
                        throw m;
                    }
                    mode = opt_ch;
                    break;
                case 'r':
                    rev = true;
                    break;
                case 'u':
                    uniq = true;
                    break;
                default:
                    throw string("Unknown $sort option: ") + opt_ch;
            }
        }
    }
    vector<string> items;
    string::size_type split = 0, split2;
    do {
        split2 = list.find('\t', split);
        items.emplace_back(list, split, split2 - split);
        split = UNSIGNED_OVERFLOW_OK(split2 + 1);
    } while (split2 != string::npos);

    if (mode != 'n') {
        if (mode == 0) {
            // String sort.
            if (!rev) {
                sort(items.begin(), items.end());
            } else {
                sort(items.begin(), items.end(),
                     [](const string& a, const string& b) {
                         return a > b;
                     });
            }
        } else {
            // "Natural" sort - embedded natural numbers are handled specially.
            if (!rev) {
                sort(items.begin(), items.end(),
                     [](const string& a, const string& b) {
                         return natcmp(a, b) < 0;
                     });
            } else {
                sort(items.begin(), items.end(),
                     [](const string& a, const string& b) {
                         return natcmp(a, b) > 0;
                     });
            }
        }

        value.reserve(list.size());
        bool tab = false;
        const string* prev = nullptr;
        for (auto&& item : items) {
            // Skip duplicates if "u" flag specified.
            if (prev && *prev == item) {
                continue;
            }
            if (uniq) {
                prev = &item;
            }

            if (tab) {
                value += '\t';
            } else {
                tab = true;
            }
            value += item;
        }
        return;
    }

    // Numeric sort.
    //
    // We first partition items such that all the negative values come first,
    // then all the zero values, then all the positive values (for a forward
    // search that is - for a reverse search the order of the partitions is
    // reversed).
    //
    // Then we sort within each partition.
    const string& decimal = option["decimal"];
    size_t part_z = 0;
    size_t part_f = items.size();

    int first = rev ? 1 : -1;
    if (!uniq) {
        // Scan linearly through items, swapping to create the desired
        // partitioning.
        size_t i = 0;
        while (i < part_f) {
            int sign = string_sign(items[i], decimal);
            if (sign == first) {
                // Swap value to end of the first partition.
                if (part_z != i)
                    swap(items[part_z], items[i]);
                ++part_z;
                ++i;
            } else if (sign == 0) {
                // Extend middle partition to include zero value.
                ++i;
            } else {
                // Swap value to start of final partition.
                swap(items[i], items[--part_f]);
            }
        }
    } else {
        // Need to preserve order within each partition.
        auto z = stable_partition(items.begin(), items.end(),
                                  [&decimal, first](const string& a) {
                                      return string_sign(a, decimal) == first;
                                  });
        part_z = z - items.begin();
        auto f = stable_partition(z, items.end(),
                                  [&decimal](const string& a) {
                                      return string_sign(a, decimal) == 0;
                                  });
        part_f = f - items.begin();
    }

    value.reserve(list.size());
    bool tab = false;

    const string* prev = nullptr;
    if (part_z > 0) {
        // Sort the first partition.
        if (!uniq) {
            if (!rev) {
                sort(items.begin(), items.begin() + part_z,
                     [&decimal](const string& a, const string& b) {
                         int r = ncmp(a, b, decimal);
                         return r ? r > 0 : a < b;
                     });
            } else {
                sort(items.begin(), items.begin() + part_z,
                     [&decimal](const string& a, const string& b) {
                         int r = ncmp(a, b, decimal);
                         return r ? r > 0 : a > b;
                     });
            }
        } else {
            stable_sort(items.begin(), items.begin() + part_z,
                 [&decimal](const string& a, const string& b) {
                     return ncmp(a, b, decimal) > 0;
                 });

            for (size_t i = 0; i != part_z; ++i) {
                const string& item = items[i];
                // Skip duplicates.
                if (prev && ncmp(*prev, item, decimal) == 0) {
                    continue;
                }
                prev = &item;

                if (tab) {
                    value += '\t';
                } else {
                    tab = true;
                }
                value += item;
            }
        }
    }

    if (part_z != part_f) {
        // Handle all the zero values.
        if (!uniq) {
            if (!rev) {
                sort(items.begin() + part_z, items.begin() + part_f,
                     [](const string& a, const string& b) {
                         return a < b;
                     });
            } else {
                sort(items.begin() + part_z, items.begin() + part_f,
                     [](const string& a, const string& b) {
                         return a > b;
                     });
            }
        } else {
            if (tab) {
                value += '\t';
            } else {
                tab = true;
            }
            // No need to actually sort this partition when uniq is true -
            // we just take the first element.
            value += items[part_z];
        }
    }

    if (part_f != items.size()) {
        // Sort the final partition.
        if (!uniq) {
            if (!rev) {
                sort(items.begin() + part_f, items.end(),
                     [&decimal](const string& a, const string& b) {
                         int r = ncmp(a, b, decimal);
                         return r ? r < 0 : a < b;
                     });
            } else {
                sort(items.begin() + part_f, items.end(),
                     [&decimal](const string& a, const string& b) {
                         int r = ncmp(a, b, decimal);
                         return r ? r < 0 : a > b;
                     });
            }
        } else {
            stable_sort(items.begin() + part_f, items.end(),
                 [&decimal](const string& a, const string& b) {
                     return ncmp(a, b, decimal) < 0;
                 });

            for (size_t i = part_f; i != items.size(); ++i) {
                const string& item = items[i];
                // Skip duplicates.
                if (prev && ncmp(*prev, item, decimal) == 0) {
                    continue;
                }
                prev = &item;

                if (tab) {
                    value += '\t';
                } else {
                    tab = true;
                }
                value += item;
            }
        }
    }

    // If uniq is true we already assembled the output in value as we processed
    // each partition.
    if (!uniq) {
        for (auto&& item : items) {
            if (tab) {
                value += '\t';
            } else {
                tab = true;
            }
            value += item;
        }
    }
}
