/*

Copyright (C) 2015-2018 Night Dive Studios, LLC.

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.

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 <http://www.gnu.org/licenses/>.

*/
/*
 * $Source: r:/prj/cit/src/RCS/schedule.c $
 * $Revision: 1.60 $
 * $Author: xemu $
 * $Date: 1994/11/22 15:39:20 $
 *
 *
 */

#include "map.h"
#include "player.h"
#include "schedule.h"
#include "grenades.h"
#include "invent.h"
#include "leanmetr.h"
#include "wares.h"
#include "objsim.h"
#include "objclass.h"
#include "otrip.h"
#include "faketime.h"
#include "trigger.h"
#include "objwpn.h"
#include "combat.h"
#include "objgame.h"
#include "objuse.h"
#include "rendtool.h"
#include "gamesys.h"
#include "frparams.h"
#include "damage.h"
#include "mainloop.h"
#include "colors.h"
#include "doorparm.h"
#include "mfdext.h"

#include "musicai.h" // for the explosion
#include "sfxlist.h"
#include "effect.h"
#include "objprop.h"

#include "frflags.h"
#include "frprotox.h"

// this will need to be initialized.
height_semaphor h_sems[NUM_HEIGHT_SEMAPHORS];

// ---------
// INTERNALS
// ---------
int expand_tstamp(ushort s);
int compare_tstamps(ushort t1, ushort t2);

void unregister_h_event(char sem);

void null_event_handler(Schedule *s, SchedEvent *ev);
void trap_event_handler(Schedule *s, SchedEvent *ev);
void height_event_handler(Schedule *s, SchedEvent *ev);
void door_event_handler(Schedule *s, SchedEvent *ev);
void grenade_event_handler(Schedule *s, SchedEvent *ev);
void explosion_event_handler(Schedule *s, SchedEvent *ev);
void exposure_event_handler(Schedule *s, SchedEvent *ev);
void light_event_handler(Schedule *s, SchedEvent *ev);
void bark_event_handler(Schedule *s, SchedEvent *ev);
void email_event_handler(Schedule *s, SchedEvent *ev);


// comparison function.
// 8/27/94 THIS FIX FOR WRAPPING BUG MAKES THE COMPARE FUNCTION MUCH LESS GENERAL.
// I.e. it relies on the fact that timestamps are in terms of TICKS2TSTAMP(player_struct.game_time)
// in the future, this should be fixed by adding more bits to the timestamp so it doesn't wrap, and
// then you can have schedule that don't go on gametime.

#define MAX_USHORT (0xFFFF)

int expand_tstamp(ushort s) {
    int gametime = TICKS2TSTAMP(player_struct.game_time);
    int stamp = s;
    if (stamp <= gametime - (MAX_USHORT / 2))
        stamp += MAX_USHORT;
    else if (stamp >= gametime + (MAX_USHORT / 2))
        stamp -= MAX_USHORT;
    return stamp;
}

int compare_tstamps(ushort t1, ushort t2) { return expand_tstamp(t1) - expand_tstamp(t2); }

int compare_events(void *e1, void *e2) {
    return compare_tstamps(((SchedEvent *)e1)->timestamp, ((SchedEvent *)e2)->timestamp);
}

// -----------------
// SUPPORT FUNCTIONS
// -----------------

void stop_terrain_elevator_sound(short sem);

uchar register_h_event(uchar x, uchar y, uchar floor, char *sem, char *key, uchar no_sfx) {
    int i, fr;

    fr = -1;
    for (i = 0; i < NUM_HEIGHT_SEMAPHORS; i++) {
        if (h_sems[i].inuse == 0)
            fr = i;
        else if (h_sems[i].x == x && h_sems[i].y == y && h_sems[i].floor == floor) {

            // conflict.  Take over the old semaphor.

			stop_terrain_elevator_sound(i);

            if (h_sems[i].key < MAX_HSEM_KEY)
                h_sems[i].key++;
            else
                h_sems[i].key = 0;

            *key = h_sems[i].key;
            *sem = i;
            if (!no_sfx) {
                h_sems[i].inuse = 2;
                if (play_digi_fx_loc(SFX_TERRAIN_ELEV_LOOP, -1, x << 8, y << 8) < 0)
                    h_sems[i].inuse = 1;
            } else
                h_sems[i].inuse = 1;
            return (TRUE);
        }
    }
    // no conflict.  Allocate new semaphor.
    if (fr < 0) {
        // no free semaphor.  Fail.
        return (FALSE);
    } else {
        h_sems[fr].x = x;
        h_sems[fr].y = y;
        h_sems[fr].floor = floor;
        h_sems[fr].key = 0;
        *key = h_sems[fr].key;
        *sem = fr;
        if (!no_sfx) {
            h_sems[fr].inuse = 2;
            if (play_digi_fx_loc(SFX_TERRAIN_ELEV_LOOP, -1, x << 8, y << 8) < 0)
                h_sems[fr].inuse = 1;
        } else
            h_sems[fr].inuse = 1;
        return (TRUE);
    }
}

void unregister_h_event(char sem) {
    if (sem < 0 || sem >= NUM_HEIGHT_SEMAPHORS)
        return;

	stop_terrain_elevator_sound(sem);

	h_sems[sem].inuse = 0;
}

// --------------
// EVENT HANDLERS
// --------------

void null_event_handler(Schedule *s, SchedEvent *ev) {}

void trap_event_handler(Schedule *s, SchedEvent *ev) {
    ObjID id1, id2;
    uchar dummy;

    id1 = ((TrapSchedEvent *)ev)->target_id;
    id2 = ((TrapSchedEvent *)ev)->source_id;

    do_multi_stuff(id1);
    if (id2 != -1)
        trap_activate(id2, &dummy);
}

#define FLOOR_HEIGHT_DELTA 0x4
void height_event_handler(Schedule *s, SchedEvent *ev) {
    MapElem *pme;
    HeightSchedEvent hse = *(HeightSchedEvent *)ev;
    short x, y;
    LGRect bounds;
    char ht, sign = (hse.steps_remaining > 0) ? 1 : -1;

    // has someone else claimed this square?
    if (h_sems[hse.semaphor].key != hse.key) {
        return;
    }
    x = h_sems[hse.semaphor].x;
    y = h_sems[hse.semaphor].y;
    pme = MAP_GET_XY(x, y);
    // look at top bit of step_size to determine floor or ceiling
    if (hse.type == CEIL_SCHED_EVENT) {
        ht = me_height_ceil(pme);
        me_height_ceil_set(pme, ht + sign);
    } else {
        ObjRefID oref;
        ObjID id;
        ObjLoc newloc;
        ht = me_height_flr(pme);
        // Change height of objects, as well...
        oref = me_objref(pme);
        while (oref != OBJ_REF_NULL) {
            // If we are on the old height, move us to the new height
            id = objRefs[oref].obj;
            if (abs(obj_floor_height(id) - objs[id].loc.z) < FLOOR_HEIGHT_DELTA) {
                if (id == PLAYER_OBJ) {
                    slam_posture_meter_state();
                } else if (ObjProps[OPNUM(id)].physics_model) {
                    newloc = objs[id].loc;
                    newloc.z = obj_floor_compute(id, ht + sign);
                    obj_move_to(id, &newloc, TRUE);
                }
            }
            // iterate
            oref = objRefs[oref].next;
        }
        // Crank us to new height;
        me_height_flr_set(pme, ht + sign);
    }

    hse.steps_remaining -= sign;
    if (hse.steps_remaining != 0) {
        hse.timestamp = TICKS2TSTAMP(player_struct.game_time + (CIT_CYCLE * HEIGHT_STEP_TIME) / HEIGHT_TIME_UNIT);
        schedule_event(&(global_fullmap->sched[MAP_SCHEDULE_GAMETIME]), (SchedEvent *)&hse);
    } else {
        unregister_h_event(hse.semaphor);
    }

    {
        bounds.ul.x = bounds.lr.x = x;
        bounds.ul.y = bounds.lr.y = y;
        rendedit_process_tilemap(global_fullmap, &bounds, FALSE);
    }
}

#define ANTENNA_DESTROYED_QVAR 0x2
#define ANTENNAE_ALL_GONE_QBIT 0x99
#define PLOTWARE_QVAR 0x9
#define NUM_ANTENNAE_TO_DESTROY 4

void door_event_handler(Schedule *s, SchedEvent *ev) {
    ObjID id;
    short old_dest;
    ushort code, curr_code;

    id = ((DoorSchedEvent *)ev)->door_id;
    code = ((DoorSchedEvent *)ev)->secret_code;
    if (objs[id].obclass == CLASS_DOOR) {
        // construct code out of top 2 bits of inst_flags
        curr_code = objs[id].info.inst_flags >> 6;

        // Make sure that we actually care about this autoclose event
        if (code == curr_code) {
            // note the secret dont-autoclose-me-even-if-I-already-
            // have-an-autoclose-scheduled cookie.
            if (!(DOOR_REALLY_CLOSED(id) || door_moving(id, TRUE) ||
                  objDoors[objs[id].specID].autoclose_time == NEVER_AUTOCLOSE_COOKIE))
                object_use(id, FALSE, OBJ_NULL);
        }
    } else if (ID2TRIP(id) == PLAS_ANTENNA_TRIPLE) {
        ObjID ground0, p3obj;
        ObjLoc blastLoc;
        ExplosionData *kaboom;
        extern short fr_sfx_time;

        // An earth-shattering kaboom.
        // turn the panel into a destroyed one
        objs[id].info.type += 1;

        // flash the screen
        fr_global_mod_flag(FR_SOLIDFR_SLDCLR, FR_SOLIDFR_MASK);
        fr_solidfr_color = GRENADE_COLOR;

        // shake yer bootie
        fr_global_mod_flag(FR_SFX_SHAKE, FR_SFX_MASK);
        fr_sfx_time = CIT_CYCLE << 1; // 2 seconds of shake

        // qvar tricks
        old_dest = QUESTVAR_GET(ANTENNA_DESTROYED_QVAR);
        QUESTVAR_SET(ANTENNA_DESTROYED_QVAR, old_dest + 1);
        if (old_dest + 1 >= NUM_ANTENNAE_TO_DESTROY) {
            QUESTVAR_SET(PLOTWARE_QVAR, QUESTVAR_GET(PLOTWARE_QVAR) + 1);
            QUESTBIT_ON(ANTENNAE_ALL_GONE_QBIT);
            do_multi_stuff(objFixtures[objs[id].specID].p1);
        }

        kaboom = &game_explosions[LARGE_GAME_EXPL];
        p3obj = ground0 = objFixtures[objs[id].specID].p3;
        if (ground0 == OBJ_NULL)
            ground0 = id;
        ObjLocCopy(objs[ground0].loc, blastLoc);
        blastLoc.z = obj_height_from_fix(fix_from_obj_height(ground0) + 4 * RAYCAST_ATTACK_SIZE);
        if (p3obj) {
            ADD_DESTROYED_OBJECT(p3obj);
            destroy_destroyed_objects();
        }
        do_explosion(blastLoc, ground0, M_EXPL2, kaboom);
        fr_global_mod_flag(FR_SFX_SHAKE, FR_SFX_MASK);
        fr_sfx_time = CIT_CYCLE << 1;
        play_digi_fx_obj(SFX_EXPLOSION_1, 1, id);
        mfd_notify_func(MFD_PLOTWARE_FUNC, MFD_ITEM_SLOT, FALSE, MFD_ACTIVE, TRUE);
    }
}

void grenade_event_handler(Schedule *s, SchedEvent *ev) {
    ObjID id;
    ubyte unique_id;

    id = ((GrenSchedEvent *)ev)->gren_id;

    if (is_obj_destroyed(id))
        return;

    // make sure we have a real grenade
    unique_id = objGrenades[objs[id].specID].unique_id;
    if ((((GrenSchedEvent *)ev)->unique_id == unique_id) && unique_id) {
        if (GrenadeProps[CPNUM(id)].flags & GREN_TIMING_TYPE)
            ADD_DESTROYED_OBJECT(id);
        //         do_grenade_explosion(id, TRUE);
        else
            EDMS_obey_collisions(objs[id].info.ph);
    }
}

void explosion_event_handler(Schedule *s, SchedEvent *ev) {}

void exposure_event_handler(Schedule *s, SchedEvent *ev) {
    SchedExposeData *xd = (SchedExposeData *)&ev->data;
    int count = xd->count;
    int damage = xd->damage;
    if (damage < 0)
        damage = (damage - 1) / 2;
    else
        damage = (damage + 1) / 2;
    expose_player(damage, xd->type, 0);
    //   count --;
    if (damage != xd->damage)
    //      if (count > 0)
    {
        SchedEvent copy;
        xd->damage -= damage;
        xd->count = (ubyte)count;
        copy = *ev;
        copy.timestamp += xd->tsecs;
        schedule_event(s, &copy);
    }
}

extern uchar muzzle_fire_light;

void light_event_handler(Schedule *s, SchedEvent *ev) {
    muzzle_fire_light = FALSE;

    // is the player's lantern on - if not turn our faux lantern off, otherwise
    // just turn the lantern on - to reset its values
    if (!(player_struct.hardwarez_status[CPTRIP(LANTERN_HARD_TRIPLE)] & WARE_ON))
        lamp_turnoff(TRUE, FALSE);
    else
        lamp_turnon(TRUE, FALSE);
}

void bark_event_handler(Schedule *s, SchedEvent *ev) {
    ubyte mfd_id;
    ubyte mfd_get_func(ubyte mfd_id, ubyte s);

    // timeout bark, if it's still there at all.
    for (mfd_id = 0; mfd_id < NUM_MFDS; mfd_id++) {
        if (mfd_get_func(mfd_id, MFD_INFO_SLOT) == MFD_BARK_FUNC) {
            check_panel_ref(TRUE);
            break;
        }
    }
}

void email_event_handler(Schedule *s, SchedEvent *ev) {
    EmailSchedEvent *e = (EmailSchedEvent *)ev;
    add_email_datamunge(e->datamunge, TRUE);
}

// HERE IS THE ARRAY OF ALL EVENT HANDLERS

static SchedHandler sched_handlers[] = {
    null_event_handler,  grenade_event_handler,  explosion_event_handler, door_event_handler,
    trap_event_handler,  exposure_event_handler, height_event_handler,    height_event_handler,
    light_event_handler, bark_event_handler,     email_event_handler,
};

#define NUM_EVENT_TYPES (sizeof(sched_handlers) / sizeof(SchedHandler))

// ---------
// EXTERNALS
// ---------

static ushort current_tstamp = 0;

errtype schedule_init(Schedule *s, int size, uchar grow) {
    TRACE("%s: schedule_init", __FUNCTION__);
    return pqueue_init(&s->queue, size, sizeof(SchedEvent), compare_events, grow);
}

errtype schedule_free(Schedule *s) {
    TRACE("%s: schedule_free", __FUNCTION__);
    return pqueue_destroy(&s->queue);
}

errtype schedule_event(Schedule *s, SchedEvent *ev) {
    errtype retval = OK;
    if (!time_passes)
        return ERR_NOEFFECT;
    if (current_tstamp > 0 && compare_tstamps(ev->timestamp, current_tstamp) < 0) {
        return ERR_NOEFFECT;
    }

    TRACE("%s: Scheduling an event.", __FUNCTION__);

    retval = pqueue_insert(&s->queue, ev);
    if (retval != OK) {
        printf("Could not schedule event?\n");
    }
    return retval;
}

errtype schedule_reset(Schedule *s) {
    s->queue.fullness = 0;
    return OK;
}

void reset_schedules(void) {
    int i;
    for (i = 0; i < NUM_MAP_SCHEDULES; i++)
        schedule_reset(&global_fullmap->sched[i]);
    schedule_reset(&game_seconds_schedule);
}

errtype schedule_run(Schedule *s, ushort time) {
    SchedEvent ev;
    errtype err;
    current_tstamp = time;
    for (err = pqueue_least(&s->queue, &ev); err == OK && compare_tstamps(ev.timestamp, time) < 0;
         err = pqueue_least(&s->queue, &ev)) {
        if (ev.type < NUM_EVENT_TYPES)
            sched_handlers[ev.type](s, &ev);
        pqueue_extract(&s->queue, &ev);
    }
    current_tstamp = 0;
    return OK;
}

void run_schedules(void) {
    schedule_run(&global_fullmap->sched[MAP_SCHEDULE_GAMETIME], TICKS2TSTAMP(player_struct.game_time));
    schedule_run(&game_seconds_schedule, TICKS2TSTAMP(player_struct.game_time));
}

/*
uchar schedule_test_hotkey(short keycode, ulong context, void* data)
{
   SchedEvent e;
#ifndef NO_DUMMIES
   int dummy; dummy = keycode + context + (int)data;
#endif
   e.timestamp = player_struct.game_time/CIT_CYCLE + 100;
   e.type = 0;
   schedule_event(&game_seconds_schedule,&e);
   return TRUE;
}
*/
