/*
 * Copyright (C) 2010 Robin Sonefors
 * Copyright (C) 2008-2012 Robert Ancell.
 *
 * 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. See http://www.gnu.org/copyleft/gpl.html the full text of the
 * license.
 */

public enum DisplayFormat
{
    AUTOMATIC,
    FIXED,
    SCIENTIFIC,
    ENGINEERING
}

public class Serializer : Object
{
    private int leading_digits;      /* Number of digits to show before radix */
    private int trailing_digits;     /* Number of digits to show after radix */
    private DisplayFormat format;    /* Number display mode. */
    private bool show_tsep;          /* Set if the thousands separator should be shown. */
    private bool show_zeroes;        /* Set if trailing zeroes should be shown. */

    private int number_base;         /* Numeric base */
    private uint representation_base;/* Representation base. */

    private unichar radix;           /* Locale specific radix string. */
    private unichar tsep;            /* Locale specific thousands separator. */
    private int tsep_count;          /* Number of digits between separator. */

    /* is set when an error (for example precision error while converting) occurs */
    public string? error { get; set; default = null; }

    public Serializer (DisplayFormat format, int number_base, int trailing_digits)
    {
        var radix_string = Posix.nl_langinfo (Posix.NLItem.RADIXCHAR);
        if (radix_string != null && radix_string != "") {
            var radix_utf8 = radix_string.locale_to_utf8 (-1, null, null);
            if (radix_utf8 != null)
                radix = radix_utf8.get_char (0);
            else
                radix = '.';
        }
        else
            radix = '.';
        var tsep_string = Posix.nl_langinfo (Posix.NLItem.THOUSEP);
        if (tsep_string != null && tsep_string != "") {
            var tsep_utf8 = tsep_string.locale_to_utf8 (-1, null, null);
            if (tsep_utf8 != null)
                tsep = tsep_utf8.get_char (0);
            else
                tsep = ' ';
        }
        else
            tsep = ' ';
        tsep_count = 3;

        this.number_base = number_base;
        this.representation_base = number_base;
        leading_digits = 12;
        this.trailing_digits = trailing_digits;
        show_zeroes = false;
        show_tsep = false;
        this.format = format;
    }

    public string to_string (Number x)
    {
        /* For base conversion equation, use FIXED format. */
        if (representation_base != number_base)
        {
            int n_digits = 0;
            return cast_to_string (x, ref n_digits);
        }
        switch (format)
        {
        default:
        case DisplayFormat.AUTOMATIC:
            int n_digits = 0;
            var s0 = cast_to_string (x, ref n_digits);
            /* Decide leading digits based on number_base. Support 64 bits in programming mode. */
            switch (get_base ())
            {
                /* 64 digits for binary mode. */
                case 2:
                    if (n_digits <= 64)
                        return s0;
                    else
                        return cast_to_exponential_string (x, false, ref n_digits);
                /* 22 digis for octal mode. */
                case 8:
                    if (n_digits <= 22)
                        return s0;
                    else
                        return cast_to_exponential_string (x, false, ref n_digits);
                /* 16 digits for hexadecimal mode. */
                case 16:
                    if(n_digits <= 16)
                        return s0;
                    else
                        return cast_to_exponential_string (x, false, ref n_digits);
                /* Use default leading_digits for base 10 numbers. */
                case 10:
                default:
                    if (n_digits <= leading_digits)
                        return s0;
                    else
                        return cast_to_exponential_string (x, false, ref n_digits);
            }
        case DisplayFormat.FIXED:
            int n_digits = 0;
            return cast_to_string (x, ref n_digits);
        case DisplayFormat.SCIENTIFIC:
            if (representation_base == 10)
            {
                int n_digits = 0;
                return cast_to_exponential_string (x, false, ref n_digits);
            }
            else
            {
                int n_digits = 0;
                return cast_to_string (x, ref n_digits);
            }
        case DisplayFormat.ENGINEERING:
            if (representation_base == 10)
            {
                int n_digits = 0;
                return cast_to_exponential_string (x, true, ref n_digits);
            }
            else
            {
                int n_digits = 0;
                return cast_to_string (x, ref n_digits);
            }
        }
    }

    public Number? from_string (string str)
    {
        // FIXME: Move mp_set_from_string into here
        return mp_set_from_string (str, number_base);
    }

    public void set_base (int number_base)
    {
        this.number_base = number_base;
    }

    public int get_base ()
    {
        return number_base;
    }

    public void set_representation_base (uint representation_base)
    {
        this.representation_base = representation_base;
    }

    public uint get_representation_base ()
    {
        return representation_base;
    }

    public void set_radix (unichar radix)
    {
        this.radix = radix;
    }

    public unichar get_radix ()
    {
        return radix;
    }

    public void set_thousands_separator (unichar separator)
    {
        tsep = separator;
    }

    public unichar get_thousands_separator ()
    {
        return tsep;
    }

    public int get_thousands_separator_count ()
    {
        return tsep_count;
    }

    public void set_thousands_separator_count (int count)
    {
        tsep_count = count;
    }

    public void set_show_thousands_separators (bool visible)
    {
        show_tsep = visible;
    }

    public bool get_show_thousands_separators ()
    {
        return show_tsep;
    }

    public void set_show_trailing_zeroes (bool visible)
    {
        show_zeroes = visible;
    }

    public bool get_show_trailing_zeroes ()
    {
        return show_zeroes;
    }

    public int get_leading_digits ()
    {
        return leading_digits;
    }

    public void set_leading_digits (int leading_digits)
    {
        this.leading_digits = leading_digits;
    }

    public int get_trailing_digits ()
    {
        return trailing_digits;
    }

    public void set_trailing_digits (int trailing_digits)
    {
        this.trailing_digits = trailing_digits;
    }

    public DisplayFormat get_number_format ()
    {
        return format;
    }

    public void set_number_format (DisplayFormat format)
    {
        this.format = format;
    }

    private string cast_to_string (Number x, ref int n_digits)
    {
        var string = new StringBuilder.sized (1024);

        var x_real = x.real_component ();
        cast_to_string_real (x_real, (int) representation_base, false, ref n_digits, string);
        if (x.is_complex ())
        {
            var x_im = x.imaginary_component ();

            var force_sign = true;
            if (string.str == "0")
            {
                string.assign ("");
                force_sign = false;
            }

            var s = new StringBuilder.sized (1024);
            int n_complex_digits = 0;
            cast_to_string_real (x_im, (int) representation_base, force_sign, ref n_complex_digits, s);
            if (n_complex_digits > n_digits)
                n_digits = n_complex_digits;
            if (s.str == "0" || s.str == "+0" || s.str == "−0")
            {
                if (string.str == "")
                {
                    string.append ("0"); // real component is empty, the imaginary very small, we shouldn't return blank
                }
            }
            else if (s.str == "1")
            {
                string.append ("i");
            }
            else if (s.str == "+1")
            {
                string.append ("+i");
            }
            else if (s.str == "−1")
            {
                string.append ("−i");
            }
            else
            {
                if (s.str == "+0")
                    string.append ("+");
                else if (s.str != "0")
                    string.append (s.str);

                string.append ("i");
            }
        }

        return string.str;
    }

    private void cast_to_string_real (Number x, int number_base, bool force_sign, ref int n_digits, StringBuilder string)
    {
        const char digits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

        var number = x;
        if (number.is_negative ())
            number = number.abs ();

        /* Add rounding factor */
        var temp = new Number.integer (number_base);
        temp = temp.xpowy_integer (-(trailing_digits+1));
        temp = temp.multiply_integer (number_base);
        temp = temp.divide_integer (2);
        var rounded_number = number.add (temp);

        /* Write out the integer component least significant digit to most */
        temp = rounded_number.floor ();
        var i = 0;
        do
        {
            if (number_base == 10 && show_tsep && i == tsep_count)
            {
                string.prepend_unichar (tsep);
                i = 0;
            }
            i++;

            var t = temp.divide_integer (number_base);
            t = t.floor ();
            var t2 = t.multiply_integer (number_base);

            var t3 = temp.subtract (t2);

            var d = t3.to_integer ();

            if (d < 16 && d >= 0)
            {
                string.prepend_c (digits[d]);
            }
            else
            {
                string.prepend_c ('?');
                error = _("Overflow: the result couldn’t be calculated");
                string.assign ("0");
                break;
            }
            n_digits++;

            temp = t;
        } while (!temp.is_zero ());

        var last_non_zero = string.len;

        string.append_unichar (radix);

        /* Write out the fractional component */
        temp = rounded_number.fractional_component ();
        for (i = 0; i < trailing_digits; i++)
        {
            if (temp.is_zero ())
                break;

            temp = temp.multiply_integer (number_base);
            var digit = temp.floor ();
            var d = digit.to_integer ();

            string.append_c (digits[d]);

            if (d != 0)
                last_non_zero = string.len;
            temp = temp.subtract (digit);
        }

        /* Strip trailing zeroes */
        if (!show_zeroes || trailing_digits == 0)
            string.truncate (last_non_zero);

        /* Add sign on non-zero values */
        if (string.str != "0" || force_sign)
        {
            if (x.is_negative ())
                string.prepend ("−");
            else if (force_sign)
                string.prepend ("+");
        }

        /* Append base suffix if not in default base */
        if (number_base != this.number_base)
        {
            const string sub_digits[] = {"₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"};
            int multiplier = 1;
            int b = number_base;

            while (number_base / multiplier != 0)
                multiplier *= 10;
            while (multiplier != 1)
            {
                int d;
                multiplier /= 10;
                d = b / multiplier;
                string.append (sub_digits[d]);
                b -= d * multiplier;
            }
        }
    }

    private int cast_to_exponential_string_real (Number x, StringBuilder string, bool eng_format, ref int n_digits)
    {
        if (x.is_negative ())
            string.append ("−");

        var mantissa = x.abs ();

        var base_ = new Number.integer (number_base);
        var base3 = base_.xpowy_integer (3);
        var base10 = base_.xpowy_integer (10);
        var t = new Number.integer (1);
        var base10inv = t.divide (base10);

        var exponent = 0;
        if (!mantissa.is_zero ())
        {
            while (!eng_format && mantissa.compare (base10) >= 0)
            {
                exponent += 10;
                mantissa = mantissa.multiply (base10inv);
            }

            while ((!eng_format && mantissa.compare (base_) >= 0) ||
                    (eng_format && (mantissa.compare (base3) >= 0 || exponent % 3 != 0)))
            {
                exponent += 1;
                mantissa = mantissa.divide (base_);
            }

            while (!eng_format && mantissa.compare (base10inv) < 0)
            {
                exponent -= 10;
                mantissa = mantissa.multiply (base10);
            }

            t = new Number.integer (1);
            while (mantissa.compare (t) < 0 || (eng_format && exponent % 3 != 0))
            {
                exponent -= 1;
                mantissa = mantissa.multiply (base_);
            }
        }

        string.append (cast_to_string (mantissa, ref n_digits));

        return exponent;
    }

    private string cast_to_exponential_string (Number x, bool eng_format, ref int n_digits)
    {
        var string = new StringBuilder.sized (1024);

        var x_real = x.real_component ();
        var exponent = cast_to_exponential_string_real (x_real, string, eng_format, ref n_digits);
        append_exponent (string, exponent);

        if (x.is_complex ())
        {
            var x_im = x.imaginary_component ();

            if (string.str == "0")
                string.assign ("");

            var s = new StringBuilder.sized (1024);
            int n_complex_digits = 0;
            exponent = cast_to_exponential_string_real (x_im, s, eng_format, ref n_complex_digits);
            if (n_complex_digits > n_digits)
                n_digits = n_complex_digits;
            if (s.str == "0" || s.str == "+0" || s.str == "−0")
            {
                /* Ignore */
            }
            else if (s.str == "1")
            {
                string.append ("i");
            }
            else if (s.str == "+1")
            {
                string.append ("+i");
            }
            else if (s.str == "−1")
            {
                string.append ("−i");
            }
            else
            {
                if (s.str == "+0")
                    string.append ("+");
                else if (s.str != "0")
                    string.append (s.str);

                string.append ("i");
            }
            append_exponent (string, exponent);
        }

        return string.str;
    }

    private void append_exponent (StringBuilder string, int exponent)
    {
        const unichar super_digits[] = {'⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'};

        if (exponent == 0)
            return;

        string.append ("×10"); // FIXME: Use the current base
        if (exponent < 0)
        {
            exponent = -exponent;
            string.append_unichar ('⁻');
        }

        var super_value = "%d".printf (exponent);
        for (var i = 0; i < super_value.length; i++)
            string.append_unichar (super_digits[super_value[i] - '0']);
    }
}
