/*
===========================================================================
Copyright (C) 2015 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code 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.

OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// characterstate.cpp:
//

#include "characterstate.h"
#include "animate.h"
#include "scriptmaster.h"

static const char *MoveControl_Names[] = {
    "none",          // MOVECONTROL_NONE
    "user",          // MOVECONTROL_USER
    "legs",          // MOVECONTROL_LEGS
    "user_moveanim", // MOVECONTROL_USER_MOVEANIM
    "anim",          // MOVECONTROL_ANIM
    "absolute",      // MOVECONTROL_ABSOLUTE
    "hanging",       // MOVECONTROL_HANGING
    "rope_grab",     // MOVECONTROL_ROPE_GRAB
    "rope_release",  // MOVECONTROL_ROPE_RELEASE
    "rope_move",     // MOVECONTROL_ROPE_MOVE
    "pickupenemy",   // MOVECONTROL_PICKUPENEMY
    "push",          // MOVECONTROL_PUSH
    "climbwall",     // MOVECONTROL_CLIMBWALL
    "useanim",       // MOVECONTROL_USEANIM
    "crouch",        // MOVECONTROL_CROUCH
    "loopuseanim",   // MOVECONTROL_LOOPUSEANIM
    "useobject",     // MOVECONTROL_USEOBJECT
    "coolobject",    // MOVECONTROL_COOLOBJECT
    NULL};

static const char *Camera_Names[] = {
    "topdown",        // CAMERA_TOPDOWN
    "behind",         // CAMERA_BEHIND
    "front",          // CAMERA_FRONT
    "side",           // CAMERA_SIDE
    "behind_fixed",   // CAMERA_BEHIND_FIXED
    "side_left",      // CAMERA_SIDE_LEFT
    "side_right",     // CAMERA_SIDE_RIGHT
    "behind_nopitch", // CAMERA_BEHIND_NOPITCH
    NULL};

Conditional::Conditional(Condition<Class>& cond)
    : condition(cond)

{
    result          = false;
    previous_result = false;
    checked         = false;
}

Conditional::Conditional()
{
    result          = false;
    previous_result = false;
    checked         = false;
}

Expression::Expression() {}

Expression::Expression(Expression& exp)

{
    int i;

    value = exp.value;

    for (i = 1; i <= exp.conditions.NumObjects(); i++) {
        conditions.AddObject(exp.conditions.ObjectAt(i));
    }
}

Expression::Expression(Script& script, State& state)

{
    str         token;
    condition_t condition;
    int         start;

    value = script.GetToken(true);

    if (!script.TokenAvailable(false) || Q_stricmp(script.GetToken(false), ":")) {
        gi.Error(ERR_DROP, "%s: Expecting ':' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(false)) {
        token = script.GetToken(true);

        switch (token[0]) {
        case '!':
            condition.test = TC_ISFALSE;
            start          = 1;
            break;

        case '+':
            condition.test = TC_EDGETRUE;
            start          = 1;
            break;

        case '-':
            condition.test = TC_EDGEFALSE;
            start          = 1;
            break;

        default:
            condition.test = TC_ISTRUE;
            start          = 0;
        }

        if (token.length() <= start) {
            gi.Error(
                ERR_DROP, "%s: Illegal syntax '%s' on line %d.\n", script.Filename(), &token, script.GetLineNumber()
            );
            condition.condition_index = 0;
            continue;
        }

        condition.condition_index = state.addCondition(&token[start], script);
        if (!condition.condition_index) {
            gi.Error(
                ERR_DROP,
                "%s: Unknown condition '%s' on line %d.\n",
                script.Filename(),
                &token[start],
                script.GetLineNumber()
            );
        }

        conditions.AddObject(condition);
    }
}

bool Expression::getResult(State& state, Entity& ent, Container<Conditional *> *sent_conditionals)

{
    int          i;
    condition_t *cond;
    Conditional *conditional;

    for (i = 1; i <= conditions.NumObjects(); i++) {
        cond        = &conditions.ObjectAt(i);
        conditional = sent_conditionals->ObjectAt(cond->condition_index);

        if (!conditional || !conditional->getResult(cond->test, ent)) {
            return false;
        }
    }

    return true;
}

void State::readNextState(Script& script)

{
    nextState = script.GetToken(false);
}

void State::readMoveType(Script& script)

{
    str          token;
    const char **name;
    int          i;

    token = script.GetToken(false);

    for (i = 0, name = MoveControl_Names; *name != NULL; name++, i++) {
        if (!token.icmp(*name)) {
            break;
        }
    }

    if (*name == NULL) {
        gi.Error(
            ERR_DROP,
            "%s: Unknown movetype '%s' on line %d.\n",
            script.Filename(),
            token.c_str(),
            script.GetLineNumber()
        );
    } else {
        movetype = (movecontrol_t)i;
    }
}

qboolean State::setCameraType(str ctype)

{
    const char **name;
    int          i;

    for (i = 0, name = Camera_Names; *name != NULL; name++, i++) {
        if (!ctype.icmp(*name)) {
            cameratype = (cameratype_t)i;
            return qtrue;
        }
    }
    return qfalse;
}

void State::readCamera(Script& script)

{
    str token;

    token = script.GetToken(false);

    if (!setCameraType(token)) {
        gi.Error(
            ERR_DROP,
            "%s: Unknown camera type '%s' on line %d.\n",
            script.Filename(),
            token.c_str(),
            script.GetLineNumber()
        );
    }
}

void State::readLegs(Script& script)

{
    str token;

    if (!script.TokenAvailable(true) || Q_stricmp(script.GetToken(true), "{")) {
        gi.Error(ERR_DROP, "%s: Expecting '{' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(true)) {
        token = script.GetToken(true);
        if (!Q_stricmp(token.c_str(), "}")) {
            break;
        }

        script.UnGetToken();
        legAnims.AddObject(Expression(script, *this));
    }
}

void State::readAction(Script& script)
{
    str token;

    m_iActionAnimType = 0;

    if (script.TokenAvailable(false)) {
        token = script.GetToken(false);
        if (token.icmp("aim")) {
            m_iActionAnimType = 1;
        }
    }

    if (!script.TokenAvailable(true)) {
        gi.Error(ERR_DROP, "%s: EOF, expected state body on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    if (!script.TokenAvailable(true) || Q_stricmp(script.GetToken(true), "{")) {
        gi.Error(ERR_DROP, "%s: Expecting '{' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(true)) {
        token = script.GetToken(true);
        if (!Q_stricmp(token.c_str(), "}")) {
            return;
        }

        script.UnGetToken();
        m_actionAnims.AddObject(Expression(script, *this));
    }

    gi.Error(ERR_DROP, "%s: EOF, expected '}' on line %d.\n", script.Filename(), script.GetLineNumber());
}

void State::readBehavior(Script& script)

{
    str token;

    if (!script.TokenAvailable(true)) {
        gi.Error(ERR_DROP, "%s: Expecting behavior name on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    behaviorName = script.GetToken(true);
    if (!getClass(behaviorName)) {
        gi.Error(
            ERR_DROP,
            "%s: Unknown behavior '%s' on line %d.\n",
            script.Filename(),
            behaviorName.c_str(),
            script.GetLineNumber()
        );
    }

    // Read in the behavior arguments if there are any

    while (script.TokenAvailable(false) && script.AtString(false)) {
        token = script.GetToken(false);
        addBehaviorParm(token);
    }
}

void State::readTime(Script& script)

{
    str token;

    if (script.TokenAvailable(false) && script.AtString(false)) {
        token   = script.GetToken(false);
        minTime = atof(token.c_str());
    }

    if (script.TokenAvailable(false) && script.AtString(false)) {
        token   = script.GetToken(false);
        maxTime = atof(token.c_str());
    } else {
        maxTime = minTime;
    }
}

void State::readStates(Script& script)

{
    str token;

    if (!script.TokenAvailable(true) || Q_stricmp(script.GetToken(true), "{")) {
        gi.Error(ERR_DROP, "%s: Expecting '{' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(true)) {
        token = script.GetToken(true);
        if (!Q_stricmp(token.c_str(), "}")) {
            break;
        }

        script.UnGetToken();
        states.AddObject(Expression(script, *this));
    }
}

void State::ParseAndProcessCommand(str command, Entity *target)

{
    int         argc;
    const char *argv[MAX_COMMANDS];
    char        args[MAX_COMMANDS][SCRIPT_MAXTOKEN];
    Script      script;
    Event      *event;

    script.Parse(command, command.length(), "CommandString");

    argc = 0;
    while (script.TokenAvailable(false)) {
        if (argc >= MAX_COMMANDS) {
            gi.DPrintf("State:ParseAndProcessCommand : Line exceeds %d command limit", MAX_COMMANDS);
            script.SkipToEOL();
            break;
        }
        Q_strncpyz(args[argc], script.GetToken(false), sizeof(args[argc]));
        argv[argc] = args[argc];
        argc++;
    }

    assert(argc > 0);

    if (argc <= 0) {
        return;
    }

    event = new Event(args[0], argc - 1);
    event->AddTokens(argc - 1, &argv[1]);
    target->ProcessEvent(event);
}

void State::ProcessEntryCommands(Entity *target)

{
    int i, count;
    str command;

    assert(target);
    if (!target) {
        return;
    }

    count = entryCommands.NumObjects();
    for (i = 1; i <= count; i++) {
        ParseAndProcessCommand(entryCommands.ObjectAt(i), target);
    }
}

void State::ProcessExitCommands(Entity *target)

{
    int i, count;
    str command;

    assert(target);
    if (!target) {
        return;
    }

    count = exitCommands.NumObjects();
    for (i = 1; i <= count; i++) {
        ParseAndProcessCommand(exitCommands.ObjectAt(i), target);
    }
}

void State::readCommands(Script& script, Container<str>& container)

{
    str token;
    str command;

    if (!script.TokenAvailable(true) || Q_stricmp(script.GetToken(true), "{")) {
        gi.Error(ERR_DROP, "%s: Expecting '{' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(true)) {
        while (script.TokenAvailable(false)) {
            token = script.GetToken(true);
            if (!Q_stricmp(token.c_str(), "}")) {
                goto out;
            }
            if (token.length()) {
                if (strstr(token.c_str(), " ") == NULL) {
                    command.append(token);
                } else {
                    command.append("\"");
                    command.append(token);
                    command.append("\"");
                }
            } else {
                command.append("\"\"");
            }

            command.append(" ");
        }
        container.AddObject(command);
        command = "";
    }
out:
    return;
}

State *State::Evaluate(Entity& ent, Container<Conditional *> *sent_conditionals)

{
    int         i;
    Expression *exp;
    State      *state;
    int         index;

    for (i = 1; i <= condition_indexes.NumObjects(); i++) {
        index = condition_indexes.ObjectAt(i);
        sent_conditionals->ObjectAt(index)->clearCheck();
        //conditions.ObjectAt( i )->clearCheck();
    }

    for (i = 1; i <= states.NumObjects(); i++) {
        exp = &states.ObjectAt(i);
        if (exp->getResult(*this, ent, sent_conditionals)) {
            state = statemap.FindState(exp->getValue());
            return state;
        }
    }

    return NULL;
}

const char *State::getLegAnim(Entity& ent, Container<Conditional *> *sent_conditionals)
{
    int         i;
    Expression *exp;
    int         index;

    for (i = 1; i <= condition_indexes.NumObjects(); i++) {
        index = condition_indexes.ObjectAt(i);
        sent_conditionals->ObjectAt(index)->clearCheck();
        //conditions.ObjectAt( i )->clearCheck();
    }

    for (i = 1; i <= legAnims.NumObjects(); i++) {
        exp = &legAnims.ObjectAt(i);
        if (exp->getResult(*this, ent, sent_conditionals)) {
            return exp->getValue();
        }
    }

    return "";
}

const char *State::getActionAnim(Entity& ent, Container<Conditional *> *sent_conditionals, int *piAnimType)
{
    int         i;
    Expression *exp;
    int         index;

    for (i = 1; i <= condition_indexes.NumObjects(); i++) {
        index = condition_indexes.ObjectAt(i);
        sent_conditionals->ObjectAt(index)->clearCheck();
    }

    for (i = 1; i <= m_actionAnims.NumObjects(); i++) {
        exp = &m_actionAnims.ObjectAt(i);
        if (exp->getResult(*this, ent, sent_conditionals)) {
            if (piAnimType) {
                *piAnimType = m_iActionAnimType;
            }

            return exp->getValue();
        }
    }

    if (piAnimType) {
        *piAnimType = 0;
    }

    return "";
}

const char *State::getBehaviorName(void)

{
    return behaviorName.c_str();
}

float State::getMinTime(void)

{
    return minTime;
}

float State::getMaxTime(void)

{
    return maxTime;
}

int State::addCondition(const char *name, Script& script)

{
    Conditional      *condition;
    Condition<Class> *cond;
    int               index;

    str token;

    condition = NULL;
    cond      = statemap.getCondition(name);
    if (!cond) {
        return 0;
    }

    condition = new Conditional(*cond);

    // Get the paramaters
    while (script.TokenAvailable(false) && script.AtString(false)) {
        token = script.GetToken(false);
        condition->addParm(token);
    }

    // only add a new conditional if a similar on doesn't exist
    index = statemap.findConditional(condition);

    if (index) {
        // delete the one we just made
        delete condition;
    } else {
        index = statemap.addConditional(condition);
    }

    condition_indexes.AddUniqueObject(index);

    return index;
}

void State::CheckStates(void)

{
    const char *value;
    int         i;

    if (!statemap.FindState(nextState.c_str())) {
        gi.Error(ERR_DROP, "Unknown next state '%s' referenced in state '%s'.\n", nextState.c_str(), getName());
    }

    for (i = 1; i <= states.NumObjects(); i++) {
        value = states.ObjectAt(i).getValue();
        if (!statemap.FindState(value)) {
            gi.Error(ERR_DROP, "Unknown state '%s' referenced in state '%s'.\n", value, getName());
        }
    }
}

void State::GetLegAnims(Container<const char *> *c)

{
    int      i, j;
    qboolean addobj = true;

    for (i = 1; i <= legAnims.NumObjects(); i++) {
        const char *value = legAnims.ObjectAt(i).getValue();
        addobj            = true;

        // Check to see if it's already in there
        for (j = 1; j <= c->NumObjects(); j++) {
            if (!Q_stricmp(c->ObjectAt(j), value)) {
                addobj = false;
                break;
            }
        }
        if (addobj) {
            c->AddObject(value);
        }
    }
}

void State::GetActionAnims(Container<const char *> *c)

{
    int      i, j;
    qboolean addobj = true;

    for (i = 1; i <= m_actionAnims.NumObjects(); i++) {
        const char *value = m_actionAnims.ObjectAt(i).getValue();
        addobj            = true;

        // Check to see if it's already in there
        for (j = 1; j <= c->NumObjects(); j++) {
            if (!Q_stricmp(c->ObjectAt(j), value)) {
                addobj = false;
                break;
            }
        }
        if (addobj) {
            c->AddObject(value);
        }
    }
}

State::State(const char *statename, Script& script, StateMap& map)
    : statemap(map)
{
    str cmd;

    name         = statename;
    nextState    = statename;
    movetype     = DEFAULT_MOVETYPE;
    cameratype   = DEFAULT_CAMERA;
    behaviorName = "idle";

    minTime = -1.0;
    maxTime = -1.0;

    if (!script.TokenAvailable(true) || Q_stricmp(script.GetToken(true), "{")) {
        gi.Error(ERR_DROP, "%s: Expecting '{' on line %d.\n", script.Filename(), script.GetLineNumber());
    }

    while (script.TokenAvailable(true)) {
        cmd = script.GetToken(true);
        if (!cmd.icmp("nextstate")) {
            readNextState(script);
        } else if (!cmd.icmp("movetype")) {
            readMoveType(script);
        } else if (!cmd.icmp("camera")) {
            readCamera(script);
        } else if (!cmd.icmp("legs")) {
            readLegs(script);
        } else if (!cmd.icmp("action")) {
            readAction(script);
        } else if (!cmd.icmp("behavior")) {
            readBehavior(script);
        } else if (!cmd.icmp("time")) {
            readTime(script);
        } else if (!cmd.icmp("states")) {
            readStates(script);
        } else if (!cmd.icmp("entrycommands")) {
            readCommands(script, entryCommands);
        } else if (!cmd.icmp("exitcommands")) {
            readCommands(script, exitCommands);
        } else if (!cmd.icmp("}")) {
            break;
        } else {
            gi.Error(
                ERR_DROP,
                "%s: Unknown command '%s' on line %d.\n",
                script.Filename(),
                cmd.c_str(),
                script.GetLineNumber()
            );
        }
    }
}

StateMap::StateMap(const char *file_name, Condition<Class> *conditions, Container<Conditional *> *conditionals)

{
    str    cmd;
    str    statename;
    State *state;
    int    i;
    Script script;

    assert(file_name);

    filename = file_name;

    this->current_conditions = conditions;

    this->current_conditionals = conditionals;

    script.LoadFile(filename);

    while (script.TokenAvailable(true)) {
        cmd = script.GetToken(true);
        if (!cmd.icmp("state")) {
            statename = script.GetToken(false);
            if (FindState(statename.c_str())) {
                gi.Error(
                    ERR_DROP,
                    "%s: Duplicate definition of state '%s' on line %d.\n",
                    filename.c_str(),
                    statename.c_str(),
                    script.GetLineNumber()
                );
            }

            // parse the state even if we already have it defined
            state = new State(statename.c_str(), script, *this);
            stateList.AddObject(state);
        } else {
            gi.Error(
                ERR_DROP,
                "%s: Unknown command '%s' on line %d.\n",
                script.Filename(),
                cmd.c_str(),
                script.GetLineNumber()
            );
        }
    }

    script.Close();

    // Have all the states check themselves to see if they reference any non-existant states.
    for (i = 1; i <= stateList.NumObjects(); i++) {
        stateList.ObjectAt(i)->CheckStates();
    }
}

StateMap::~StateMap()
{
    int i, num;

    num = stateList.NumObjects();
    for (i = num; i > 0; i--) {
        delete stateList.ObjectAt(i);
    }
    stateList.FreeObjectList();
}

Condition<Class> *StateMap::getCondition(const char *name)

{
    Condition<Class> *c;

    if (current_conditions) {
        for (c = current_conditions; c->name; c++) {
            if (!strcmp(c->name, name)) {
                return c;
            }
        }
    }

    return NULL;
}

int StateMap::findConditional(Conditional *condition)

{
    int          i;
    int          j;
    Conditional *c;
    bool         found;

    // Check for the one special case where we don't want to merge the conditionals

    if (strcmp(condition->getName(), "CHANCE") == 0) {
        return 0;
    }

    for (i = 1; i <= current_conditionals->NumObjects(); i++) {
        c = current_conditionals->ObjectAt(i);
        if ((c->getName() == condition->getName()) && (c->numParms() == condition->numParms())) {
            found = true;
            for (j = 1; j <= c->numParms(); j++) {
                if (strcmp(c->getParm(j), condition->getParm(j))) {
                    found = false;
                    break;
                }
            }

            if (found) {
                return i;
            }
        }
    }

    return 0;
}

int StateMap::addConditional(Conditional *condition)

{
    int index;
    index = current_conditionals->AddObject(condition);

    return index;
}

Conditional *StateMap::getConditional(const char *name)

{
    int               i;
    Conditional      *c;
    Condition<Class> *condition;

    for (i = 1; i <= current_conditionals->NumObjects(); i++) {
        c = current_conditionals->ObjectAt(i);
        if (!strcmp(c->getName(), name)) {
            return c;
        }
    }

    condition = getCondition(name);

    c = new Conditional(*condition);
    current_conditionals->AddObject(c);

    return c;
}

State *StateMap::FindState(const char *name)

{
    int i;

    for (i = 1; i <= stateList.NumObjects(); i++) {
        if (!strcmp(stateList.ObjectAt(i)->getName(), name)) {
            return stateList.ObjectAt(i);
        }
    }

    return NULL;
}

// Caching statemaps

struct cached_statemap_t {
    StateMap                 *statemap;
    Container<Conditional *> *conditionals;
};

Container<cached_statemap_t> cached_statemaps;

StateMap *GetStatemap(
    str                       filename,
    Condition<Class>         *conditions,
    Container<Conditional *> *conditionals,
    qboolean                  reload,
    qboolean                  cache_only
)

{
    int                i;
    int                j;
    cached_statemap_t *cache = NULL;
    cached_statemap_t  new_cache;
    qboolean           found = false;
    Conditional       *new_conditional;
    Conditional       *old_conditional;
    Condition<Class>  *cond;

    for (i = 1; i <= cached_statemaps.NumObjects(); i++) {
        cache = &cached_statemaps.ObjectAt(i);

        if (strcmp(cache->statemap->Filename(), filename.c_str()) == 0) {
            found = true;
            break;
        }
    }

    if (found && reload) {
        delete cache->statemap;
        delete cache->conditionals;

        cache->conditionals = new Container<Conditional *>;
        cache->statemap     = new StateMap(filename, conditions, cache->conditionals);
    }

    if (!found) {
        new_cache.conditionals = new Container<Conditional *>;
        new_cache.statemap     = new StateMap(filename, conditions, new_cache.conditionals);

        cached_statemaps.AddObject(new_cache);

        cache = &new_cache;
    }

    // Copy conditionals over

    if (!cache_only) {
        for (i = 1; i <= cache->conditionals->NumObjects(); i++) {
            old_conditional = cache->conditionals->ObjectAt(i);

            cond = cache->statemap->getCondition(old_conditional->condition.name);

            new_conditional = new Conditional(*cond);

            for (j = 1; j <= old_conditional->parmList.NumObjects(); j++) {
                new_conditional->parmList.AddObject(old_conditional->parmList.ObjectAt(j));
            }

            conditionals->AddObject(new_conditional);
        }
    }

    return cache->statemap;
}

void CacheStatemap(str filename, Condition<Class> *conditions)

{
    GetStatemap(filename, conditions, NULL, false, true);
}

void StateMap::GetAllAnims(Container<const char *> *c)

{
    int i;

    for (i = 1; i <= stateList.NumObjects(); i++) {
        stateList.ObjectAt(i)->GetLegAnims(c);
        stateList.ObjectAt(i)->GetActionAnims(c);
    }
}

void ClearCachedStatemaps(void)

{
    int                i, j, num2;
    cached_statemap_t *cache;

    num2 = cached_statemaps.NumObjects();

    for (i = num2; i > 0; i--) {
        cache = &cached_statemaps.ObjectAt(i);

        delete cache->statemap;

        int num = cache->conditionals->NumObjects();
        for (j = num; j > 0; j--) {
            Conditional *cond = cache->conditionals->ObjectAt(j);
            delete cond;
        }
        delete cache->conditionals;
    }

    cached_statemaps.FreeObjectList();
}
