/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * Copyright (C) 2010-2014 Robert Ancell
 * Copyright (C) 2015-2016 Sahil Sareen
 *
 * 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.
 */

/* This is the Seven Tag Roster (STR). They have to appear at the top, in this order. */
private int str_index (string name)
{
    if (name == "Event")
        return 0;
    else if (name == "Site")
        return 1;
    else if (name == "Date")
        return 2;
    else if (name == "Round")
        return 3;
    else if (name == "White")
        return 4;
    else if (name == "Black")
        return 5;
    else if (name == "Result")
        return 6;
    else
        return 7;
}

private int compare_tag (string name0, string name1)
{
    int str_index0 = str_index (name0);
    int str_index1 = str_index (name1);

    /* If both not in STR then just order alphabetically */
    if (str_index0 == 7 && str_index1 == 7)
        return strcmp (name0, name1);
    else
        return str_index0 - str_index1;
}

public errordomain PGNError
{
    LOAD_ERROR
}

public class PGNGame : Object
{
    public HashTable<string, string> tags;
    public List<string> moves;

    public static string RESULT_IN_PROGRESS = "*";
    public static string RESULT_DRAW        = "1/2-1/2";
    public static string RESULT_WHITE       = "1-0";
    public static string RESULT_BLACK       = "0-1";

    public static string TERMINATE_ABANDONED        = "abandoned";
    public static string TERMINATE_ADJUDICATION     = "adjudication";
    public static string TERMINATE_DEATH            = "death";
    public static string TERMINATE_EMERGENCY        = "emergency";
    public static string TERMINATE_NORMAL           = "normal";
    public static string TERMINATE_RULES_INFRACTION = "rules infraction";
    public static string TERMINATE_TIME_FORFEIT     = "time forfeit";
    public static string TERMINATE_UNTERMINATED     = "unterminated";

    public string event
    {
        get { return tags.lookup ("Event"); }
        set { tags.insert ("Event", value); }
    }
    public string site
    {
        get { return tags.lookup ("Site"); }
        set { tags.insert ("Site", value); }
    }
    public string date
    {
        get { return tags.lookup ("Date"); }
        set { tags.insert ("Date", value); }
    }
    public string time
    {
        get { return tags.lookup ("Time"); }
        set { tags.insert ("Time", value); }
    }
    public string round
    {
        get { return tags.lookup ("Round"); }
        set { tags.insert ("Round", value); }
    }
    public string white
    {
        get { return tags.lookup ("White"); }
        set { tags.insert ("White", value); }
    }
    public string black
    {
        get { return tags.lookup ("Black"); }
        set { tags.insert ("Black", value); }
    }
    public string result
    {
        get { return tags.lookup ("Result"); }
        set { tags.insert ("Result", value); }
    }
    public string? annotator
    {
        get { return tags.lookup ("Annotator"); }
        set { tags.insert ("Annotator", value); }
    }
    public string? time_control
    {
        get { return tags.lookup ("TimeControl"); }
        set { tags.insert ("TimeControl", value); }
    }
    public string? white_time_left
    {
        get { return tags.lookup ("X-GNOME-WhiteTimeLeft"); }
        set { tags.insert ("X-GNOME-WhiteTimeLeft", value); }
    }
    public string? black_time_left
    {
        get { return tags.lookup ("X-GNOME-BlackTimeLeft"); }
        set { tags.insert ("X-GNOME-BlackTimeLeft", value); }
    }
    public string? clock_type
    {
        get { return tags.lookup ("X-GNOME-ClockType"); }
        set { tags.insert ("X-GNOME-ClockType", value); }
    }
    public string? timer_increment
    {
        get { return tags.lookup ("X-GNOME-TimerIncrement"); }
        set { tags.insert ("X-GNOME-TimerIncrement", value); }
    }
    public bool set_up
    {
        get { string? v = tags.lookup ("SetUp"); return v != null && v == "1" ? true : false; }
        set { tags.insert ("SetUp", value ? "1" : "0"); }
    }
    public string? fen
    {
        get { return tags.lookup ("FEN"); }
        set { tags.insert ("FEN", value); }
    }
    public string? termination
    {
        get { return tags.lookup ("Termination"); }
        set { tags.insert ("Termination", value); }
    }
    public string? white_ai
    {
        get { return tags.lookup ("X-GNOME-WhiteAI"); }
        set { tags.insert ("X-GNOME-WhiteAI", value); }
    }
    public string? white_level
    {
        get { return tags.lookup ("X-GNOME-WhiteLevel"); }
        set { tags.insert ("X-GNOME-WhiteLevel", value); }
    }
    public string? black_ai
    {
        get { return tags.lookup ("X-GNOME-BlackAI"); }
        set { tags.insert ("X-GNOME-BlackAI", value); }
    }
    public string? black_level
    {
        get { return tags.lookup ("X-GNOME-BlackLevel"); }
        set { tags.insert ("X-GNOME-BlackLevel", value); }
    }

    public PGNGame ()
    {
        tags = new HashTable<string, string> (str_hash, str_equal);
        tags.insert ("Event", "?");
        tags.insert ("Site", "?");
        tags.insert ("Date", "????.??.??");
        tags.insert ("Round", "?");
        tags.insert ("White", "?");
        tags.insert ("Black", "?");
        tags.insert ("Result", PGNGame.RESULT_IN_PROGRESS);
    }

    public string escape (string value)
    {
        var a = value.replace ("\\", "\\\\");
        return a.replace ("\"", "\\\"");
    }

    public void write (File file) throws Error
    {
        var data = new StringBuilder ();

        var keys = tags.get_keys ();
        keys.sort ((CompareFunc) compare_tag);
        foreach (var key in keys)
            data.append ("[%s \"%s\"]\n".printf (key, escape (tags.lookup (key))));
        data.append ("\n");

        int i = 0;
        foreach (string move in moves)
        {
            if (i % 2 == 0)
                data.append ("%d. ".printf (i / 2 + 1));
            data.append (move);
            data.append (" ");
            i++;
        }
        data.append (result);
        data.append ("\n");

        file.replace_contents (data.str.data, null, false, FileCreateFlags.NONE, null);
    }
}

enum State
{
    TAGS,
    MOVE_TEXT,
    LINE_COMMENT,
    BRACE_COMMENT,
    TAG_START,
    TAG_NAME,
    PRE_TAG_VALUE,
    TAG_VALUE,
    POST_TAG_VALUE,
    SYMBOL,
    PERIOD,
    NAG,
    ERROR
}

public class PGN : Object
{
    public List<PGNGame> games;
    private void insert_tag (PGNGame game, string tag_name, string tag_value)
    {
        switch (tag_name)
        {
        case "TimeControl":
            if (int64.try_parse (tag_value) == true)
                game.tags.insert ("TimeControl", tag_value);
            else
                warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
            break;
        case "WhiteTimeLeft":
        case "X-GNOME-WhiteTimeLeft":
            if (int64.try_parse (tag_value) == false)
                warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
            else
                game.tags.insert ("X-GNOME-WhiteTimeLeft", tag_value);
            break;
        case "BlackTimeLeft":
        case "X-GNOME-BlackTimeLeft":
            if (int64.try_parse (tag_value) == false)
                warning (_("Invalid %s : %s in PGN, setting timer to infinity."), tag_name, tag_value);
            else
                game.tags.insert ("X-GNOME-BlackTimeLeft", tag_value);
            break;
        case "X-GNOME-ClockType":
            if (ChessClockType.string_to_enum (tag_value) == ChessClockType.INVALID)
            {
                warning (_("Invalid clock type in PGN: %s, using a simple clock."), tag_value);
                game.tags.insert ("X-GNOME-ClockType", "simple");
            }
            break;
        case "X-GNOME-TimerIncrement":
            if (int64.try_parse (tag_value) == false)
            {
                warning (_("Invalid timer increment in PGN: %s, using a simple clock."), tag_value);
                game.tags["X-GNOME-ClockType"] = "simple";
                game.tags.insert ("X-GNOME-TimerIncrement", "0");
            }
            break;
        case "WhiteAI":
        case "X-GNOME-WhiteAI":
            game.tags.insert ("X-GNOME-WhiteAI", tag_value);
            break;
        case "WhiteLevel":
        case "X-GNOME-WhiteLevel":
            game.tags.insert ("X-GNOME-WhiteLevel", tag_value);
            break;
        case "BlackAI":
        case "X-GNOME-BlackAI":
            game.tags.insert ("X-GNOME-BlackAI", tag_value);
            break;
        case "BlackLevel":
        case "X-GNOME-BlackLevel":
            game.tags.insert ("X-GNOME-BlackLevel", tag_value);
            break;
        default:
            game.tags.insert (tag_name, tag_value);
            break;
        }
    }

    public PGN.from_string (string data) throws PGNError
    {
        // FIXME: Feed through newline at end to make sure parsing complete

        State state = State.TAGS, home_state = State.TAGS;
        PGNGame game = new PGNGame ();
        bool in_escape = false;
        size_t token_start = 0, line_offset = 0;
        string tag_name = "";
        StringBuilder tag_value = new StringBuilder ();
        int line = 1;
        int rav_level = 0;
        for (size_t offset = 0; offset <= data.length; offset++)
        {
            unichar c = data[(long) offset];

            if (c == '\n')
            {
               line++;
               line_offset = offset + 1;
            }

            switch (state)
            {
            case State.TAGS:
                home_state = State.TAGS;
                if (c == ';')
                    state = State.LINE_COMMENT;
                else if (c == '{')
                    state = State.BRACE_COMMENT;
                else if (c == '[')
                    state = State.TAG_START;
                else if (!c.isspace ())
                {
                    offset--;
                    state = State.MOVE_TEXT;
                    continue;
                }
                break;

            case State.MOVE_TEXT:
                home_state = State.MOVE_TEXT;
                if (c == ';')
                    state = State.LINE_COMMENT;
                else if (c == '{')
                    state = State.BRACE_COMMENT;
                else if (c == '*')
                {
                    if (rav_level == 0)
                    {
                        game.result = PGNGame.RESULT_IN_PROGRESS;
                        games.append (game);
                        game = new PGNGame ();
                        state = State.TAGS;
                    }
                }
                else if (c == '.')
                {
                    offset--;
                    state = State.PERIOD;
                }
                else if (c.isalnum ())
                {
                    token_start = offset;
                    state = State.SYMBOL;
                }
                else if (c == '$')
                {
                    token_start = offset + 1;
                    state = State.NAG;
                }
                else if (c == '(')
                {
                    rav_level++;
                    continue;
                }
                else if (c == ')')
                {
                    if (rav_level == 0)
                        state = State.ERROR;
                    else
                        rav_level--;
                }
                else if (!c.isspace ())
                    state = State.ERROR;
                break;

            case State.LINE_COMMENT:
                if (c == '\n')
                    state = home_state;
                break;

            case State.BRACE_COMMENT:
                if (c == '}')
                    state = home_state;
                break;

            case State.TAG_START:
                if (c.isspace ())
                    continue;
                else if (c.isalnum ())
                {
                    token_start = offset;
                    state = State.TAG_NAME;
                }
                else
                    state = State.ERROR;
                break;

            case State.TAG_NAME:
                if (c.isspace ())
                {
                    tag_name = data[(long) token_start:(long) offset];
                    state = State.PRE_TAG_VALUE;
                }
                else if (c.isalnum () || c == '_' || c == '+' || c == '#' || c == '=' || c == ':' || c == '-')
                    continue;
                else
                    state = State.ERROR;
                break;

            case State.PRE_TAG_VALUE:
                if (c.isspace ())
                    continue;
                else if (c == '"')
                {
                    state = State.TAG_VALUE;
                    tag_value.erase ();
                    in_escape = false;
                }
                else
                    state = State.ERROR;
                break;

            case State.TAG_VALUE:
                if (c == '\\' && !in_escape)
                    in_escape = true;
                else if (c == '"' && !in_escape)
                    state = State.POST_TAG_VALUE;
                else if (c.isprint ())
                {
                    tag_value.append_unichar (c);
                    in_escape = false;
                }
                else
                    state = State.ERROR;
                break;

            case State.POST_TAG_VALUE:
                if (c.isspace ())
                    continue;
                else if (c == ']')
                {
                    insert_tag (game, tag_name, tag_value.str);
                    state = State.TAGS;
                }
                else
                    state = State.ERROR;
                break;

            case State.SYMBOL:
                /* NOTE: '/' not in spec but required for 1/2-1/2 symbol */
                if (c.isalnum () || c == '_' || c == '+' || c == '#' || c == '=' || c == ':' || c == '-' || c == '/')
                    continue;
                else
                {
                    string symbol = data[(long) token_start:(long) offset];

                    bool is_number = true;
                    for (int i = 0; i < symbol.length; i++)
                       if (!symbol[i].isdigit ())
                           is_number = false;

                    state = State.MOVE_TEXT;
                    offset--;

                    /* Game termination markers */
                    if (symbol == PGNGame.RESULT_DRAW || symbol == PGNGame.RESULT_WHITE || symbol == PGNGame.RESULT_BLACK)
                    {
                        if (rav_level == 0)
                        {
                            game.result = symbol;
                            games.append (game);
                            game = new PGNGame ();
                            state = State.TAGS;
                        }
                    }
                    else if (!is_number)
                    {
                        if (rav_level == 0)
                            game.moves.append (symbol);
                    }
                }
                break;

            case State.PERIOD:
                /* FIXME: Should check these move carefully, e.g. "1. e2" */
                state = State.MOVE_TEXT;
                break;

            case State.NAG:
                if (c.isdigit ())
                    continue;
                else
                {
                    state = State.MOVE_TEXT;
                    offset--;
                }
                break;

            case State.ERROR:
                size_t char_offset = offset - line_offset - 1;
                stderr.printf ("%d.%d: error: Unexpected character\n", line, (int) (char_offset + 1));
                stderr.printf ("%s\n", data[(long) line_offset:(long) offset]);
                for (int i = 0; i < char_offset; i++)
                    stderr.printf (" ");
                stderr.printf ("^\n");
                return;
            }
        }

        if (game.moves.length () > 0 || game.tags.size () > 0)
            games.append (game);

        /* Must have at least one game */
        if (games == null)
            throw new PGNError.LOAD_ERROR ("No games in PGN file");
    }

    public PGN.from_file (File file) throws Error
    {
        uint8[] contents;
        file.load_contents (null, out contents, null);
        this.from_string ((string) contents);
    }
}
