/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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/>.
 *
 */

#ifndef DIRECTOR_LINGO_LINGO_H
#define DIRECTOR_LINGO_LINGO_H

namespace Audio {
class AudioStream;
}
namespace Common {
class SeekableReadStreamEndian;
}

namespace Director {

struct ChunkReference;
struct MenuReference;
struct PictureReference;
struct TheEntity;
struct TheEntityField;
struct LingoArchive;
struct LingoV4Bytecode;
struct LingoV4TheEntity;
struct Node;
struct Picture;
class AbstractObject;
class Cast;
class ScriptContext;
class DirectorEngine;
class Frame;
class LingoCompiler;
struct Breakpoint;

typedef void (*inst)(void);
#define	STOP (inst)0
#define ENTITY_INDEX(t,id) ((t) * 100000 + (id))

int calcStringAlignment(const char *s);
int calcCodeAlignment(int l);

typedef Common::Array<inst> ScriptData;

struct FuncDesc {
	Common::String name;
	const char *proto;

	FuncDesc(Common::String n, const char *p) { name = n; proto = p; }
};

typedef Common::HashMap<void *, FuncDesc *> FuncHash;

typedef Common::HashMap<Common::String, bool, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> MethodHash;

struct BuiltinProto {
	const char *name;
	void (*func)(int);
	int minArgs;	// -1 -- arglist
	int maxArgs;
	int version;
	SymbolType type;
};

struct Symbol {	/* symbol table entry */
	Common::String *name;
	SymbolType type;
	union {
		ScriptData	*defn;	/* HANDLER */
		void (*func)();		/* OPCODE */
		void (*bltin)(int);	/* BUILTIN */
		Common::String	*s;	/* STRING */
	} u;

	int *refCount;

	int nargs;		/* number of arguments */
	int maxArgs;	/* maximal number of arguments, for builtins */
	int targetType;	/* valid target objects, for method builtins */

	Common::Array<Common::String> *argNames;
	Common::Array<Common::String> *varNames;
	ScriptContext *ctx;		/* optional script context to execute with */
	AbstractObject *target;			/* optional method target */
	bool anonymous;

	Symbol();
	Symbol(const Symbol &s);
	Symbol& operator=(const Symbol &s);
	bool operator==(Symbol &s) const;
	void reset();
	~Symbol();
};

struct PArray {
	bool _sorted;
	PropertyArray arr;

	PArray() : _sorted(false) {}

	PArray(int size) : _sorted(false), arr(size) {}
};

struct FArray {
	bool _sorted;
	DatumArray arr;

	FArray() : _sorted(false) {}

	FArray(int size) : _sorted(false), arr(size) {}
};


struct Datum {	/* interpreter stack type */
	DatumType type;

	union {
		int	i;				/* INT, ARGC, ARGCNORET */
		double f;			/* FLOAT */
		Common::String *s;	/* STRING, VARREF, OBJECT */
		FArray *farr;	/* ARRAY, POINT, RECT */
		PArray *parr; /* PARRAY */
		AbstractObject *obj; /* OBJECT */
		ChunkReference *cref; /* CHUNKREF */
		CastMemberID *cast;	/* CASTREF, FIELDREF */
		MenuReference *menu; /* MENUREF	*/
		PictureReference *picture; /* PICTUREREF */
	} u;

	int *refCount;

	bool ignoreGlobal; // True if this Datum should be ignored by showGlobals and clearGlobals

	Datum();
	Datum(const Datum &d);
	Datum& operator=(const Datum &d);
	Datum(int val);
	Datum(double val);
	Datum(const Common::String &val);
	Datum(AbstractObject *val);
	Datum(const CastMemberID &val);
	Datum(const Common::Point &point);
	Datum(const Common::Rect &rect);
	void reset();

	~Datum() {
		reset();
	}

	Datum eval() const;
	double asFloat() const;
	int asInt() const;
	Common::String asString(bool printonly = false) const;
	CastMemberID asMemberID(CastType castType = kCastTypeAny, int castLib = 0) const;
	Common::Point asPoint() const;
	Datum clone() const;

	bool isRef() const;
	bool isVarRef() const;
	bool isCastRef() const;
	bool isArray() const;
	bool isNumeric() const;

	const char *type2str(bool ilk = false) const;

	int equalTo(const Datum &d, bool ignoreCase = false) const;
	uint32 compareTo(const Datum &d) const;

	bool operator==(const Datum &d) const;
	bool operator>(const Datum &d) const;
	bool operator<(const Datum &d) const;
	bool operator>=(const Datum &d) const;
	bool operator<=(const Datum &d) const;
};

struct ChunkReference {
	Datum source;
	ChunkType type;
	int startChunk;
	int endChunk;
	int start;
	int end;

	ChunkReference(const Datum &src, ChunkType t, int sc, int ec, int s, int e)
		: source(src), type(t), startChunk(sc), endChunk(ec), start(s), end(e) {}
};

struct MenuReference {
	int menuIdNum;
	Common::String *menuIdStr;
	int menuItemIdNum;
	Common::String *menuItemIdStr;

	MenuReference();
};

struct PictureReference {
	Picture *_picture = nullptr;
	~PictureReference();
};

struct PCell {
	Datum p;
	Datum v;

	PCell();
	PCell(const Datum &prop, const Datum &val);
};

struct Builtin {
	void (*func)(void);
	int nargs;

	Builtin(void (*func1)(void), int nargs1) : func(func1), nargs(nargs1) {}
};

typedef Common::HashMap<int32, ScriptContext *> ScriptContextHash;
typedef Common::HashMap<int32, Common::HashMap<Common::String, ScriptContext *> *> FactoryContextHash;
typedef Common::Array<Datum> StackData;
typedef Common::HashMap<Common::String, Symbol, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> SymbolHash;
typedef Common::HashMap<Common::String, Datum, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> DatumHash;
typedef Common::HashMap<Common::String, Builtin *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> BuiltinHash;
typedef Common::HashMap<Common::String, VarType, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> VarTypeHash;
typedef void (*XLibOpenerFunc)(ObjectType, const Common::Path &);
typedef void (*XLibCloserFunc)(ObjectType);
typedef Common::HashMap<Common::String, XLibOpenerFunc, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> XLibOpenerFuncHash;
typedef Common::HashMap<Common::String, XLibCloserFunc, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> XLibCloserFuncHash;
typedef Common::HashMap<Common::String, ObjectType, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> OpenXLibsHash;
typedef Common::HashMap<Common::String, AbstractObject *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> OpenXLibsStateHash;

typedef Common::HashMap<Common::String, const TheEntity *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> TheEntityHash;
typedef Common::HashMap<Common::String, const TheEntityField *, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> TheEntityFieldHash;

struct CFrame {	/* proc/func call stack frame */
	Symbol			sp;					/* symbol table entry */
	int				retPC;				/* where to resume after return */
	ScriptData		*retScript;			/* which script to resume after return */
	ScriptContext	*retContext;		/* which script context to use after return */
	DatumHash		*retLocalVars;
	Datum			retMe;				/* which me obj to use after return */
	uint			stackSizeBefore;
	bool			allowRetVal;		/* whether to allow a return value */
	Datum			defaultRetVal;		/* default return value */
	int				paramCount;			/* original number of arguments submitted */
	Common::Array<Datum> paramList;		/* original argument list */
};

struct LingoEvent {
	LEvent event;
	int eventId;
	EventHandlerSourceType eventHandlerSourceType;
	ScriptType scriptType;
	bool passByDefault;
	uint16 channelId;
	CastMemberID scriptId;
	Common::Point mousePos;

	LingoEvent (LEvent e, int ei, ScriptType st, bool pass, CastMemberID si = CastMemberID(), Common::Point mp = Common::Point(-1, -1)) {
		event = e;
		eventId = ei;
		eventHandlerSourceType = kNoneHandler;
		scriptType = st;
		passByDefault = pass;
		channelId = 0;
		scriptId = si;
		mousePos = mp;
	}

	LingoEvent (LEvent e, int ei, EventHandlerSourceType ehst, bool pass, Common::Point mp = Common::Point(-1, -1)) {
		event = e;
		eventId = ei;
		eventHandlerSourceType = ehst;
		scriptType = kNoneScript;
		passByDefault = pass;
		channelId = 0;
		scriptId = CastMemberID();
		mousePos = mp;
	}
};


struct LingoArchive {
	LingoArchive(Cast *c) : cast(c) {};
	~LingoArchive();

	Cast *cast;
	ScriptContextHash lctxContexts;
	ScriptContextHash scriptContexts[kMaxScriptType + 1];
	FactoryContextHash factoryContexts;
	Common::Array<Common::String> names;
	Common::HashMap<uint32, Common::String> primaryEventHandlers;
	SymbolHash functionHandlers;

	ScriptContext *getScriptContext(ScriptType type, uint16 id);
	ScriptContext *findScriptContext(uint16 id);
	Common::String getName(uint16 id);
	Common::String formatFunctionList(const char *prefix);

	void addCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr, uint32 preprocFlags = kLPPNone);
	void patchCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr, uint32 preprocFlags = kLPPNone);
	void removeCode(ScriptType type, uint16 id);
	void replaceCode(const Common::U32String &code, ScriptType type, uint16 id, const char *scriptName = nullptr);
	void addCodeV4(Common::SeekableReadStreamEndian &stream, uint16 lctxIndex, const Common::String &archName, uint16 version);
	void addNamesV4(Common::SeekableReadStreamEndian &stream);

	// lingo-patcher.cpp
	void patchScriptHandler(ScriptType type, CastMemberID id);
};

struct LingoState {
	// Execution state for a Lingo process, created every time
	// a top-level handler is called (e.g. on mouseDown).
	// Can be swapped out when another script gets called with priority.
	// Call frames are pushed and popped from the callstack with
	// pushContext and popContext.
	Common::Array<CFrame *> callstack;		// call stack
	uint pc = 0;							// current program counter
	ScriptData *script = nullptr;			// current Lingo script
	ScriptContext *context = nullptr;		// current Lingo script context
	DatumHash *localVars = nullptr;			// current local variables
	Datum me;								// current me object
	StackData stack;

	~LingoState();
};

enum LingoExecState {
	kRunning,
	kPause,
};

class Lingo {

public:
	Lingo(DirectorEngine *vm);
	~Lingo();

	void resetLingo();
	void cleanupLingo();
	void resetLingoGo();

	int getMenuNum();
	int getMenuItemsNum(Datum &d);
	int getXtrasNum();
	int getCastLibsNum();
	int getMembersNum(uint16 castLibID);

	void executeHandler(const Common::String &name, int numargs = 0);
	void executeScript(ScriptType type, CastMemberID id);
	Common::String formatStack();
	void printStack(const char *s, uint pc);
	Common::String formatCallStack(uint pc);
	void printCallStack(uint pc);
	Common::String formatFrame();
	Common::String formatCurrentInstruction();
	Common::String decodeInstruction(ScriptData *sd, uint pc, uint *newPC = NULL);
	Common::String decodeScript(ScriptData *sd);
	Common::String formatFunctionName(Symbol &sym);
	Common::String formatFunctionBody(Symbol &sym);

	void reloadBuiltIns();
	void initBuiltIns();
	void initBuiltIns(const BuiltinProto protos[]);
	void cleanupBuiltIns();
	void cleanupBuiltIns(const BuiltinProto protos[]);
	void initFuncs();
	void cleanupFuncs();
	void initBytecode();
	void initMethods();
	void cleanupMethods();
	void initXLibs();
	void cleanupXLibs();

	Common::String normalizeXLibName(Common::String name);
	void openXLib(Common::String name, ObjectType type, const Common::Path &path);
	void closeXLib(Common::String name);
	void closeOpenXLibs();
	void reloadOpenXLibs();

	void runTests();

	// lingo-events.cpp
private:
	void initEventHandlerTypes();
	bool processEvent(LEvent event, ScriptType st, CastMemberID scriptId, int channelId = -1);

public:
	ScriptType event2script(LEvent ev);
	Symbol getHandler(const Common::String &name);

	void processEvents(Common::Queue<LingoEvent> &queue, bool isInputEvent);

public:
	bool execute(int targetFrame = -1);
	void switchStateFromWindow();
	void freezeState();
	void freezePlayState();
	void pushContext(const Symbol funcSym, bool allowRetVal, Datum defaultRetVal, int paramCount, int nargs);
	void popContext(bool aborting = false);
	void cleanLocalVars();
	void varAssign(const Datum &var, const Datum &value);
	Datum varFetch(const Datum &var, bool silent = false);
	Common::U32String evalChunkRef(const Datum &var);
	Datum findVarV4(int varType, const Datum &id);
	CastMemberID resolveCastMember(const Datum &memberID, const Datum &castLib, CastType type);
	CastMemberID toCastMemberID(const Datum &member, const Datum &castLib);
	void exposeXObject(const char *name, Datum obj);

	int getAlignedType(const Datum &d1, const Datum &d2, bool equality);

	Common::String formatAllVars();
	void printAllVars();

	inst readInst() { return getInst(_state->pc++); }
	inst getInst(uint pc) { return (*_state->script)[pc]; }
	int readInt() { return getInt(_state->pc++); }
	int getInt(uint pc);
	double readFloat() { double d = getFloat(_state->pc); _state->pc += calcCodeAlignment(sizeof(double)); return d; }
	double getFloat(uint pc) { return *(double *)(&((*_state->script)[_state->pc])); }
	char *readString() { char *s = getString(_state->pc); _state->pc += calcStringAlignment(s); return s; }
	char *getString(uint pc) { return (char *)(&((*_state->script)[_state->pc])); }

	Datum getVoid();
	void pushVoid();

	void printArgs(const char *funcname, int nargs, const char *prefix = nullptr);
	inline void printSTUBWithArglist(const char *funcname, int nargs) { printArgs(funcname, nargs, "STUB: "); }
	void convertVOIDtoString(int arg, int nargs);
	void dropStack(int nargs);
	void drop(uint num);

	void lingoError(const char *s, ...);

	void func_mci(const Common::String &name);
	void func_mciwait(const Common::String &name);
	void func_beep(int repeats);
	void func_goto(Datum &frame, Datum &movie, bool commandgo = false );
	void func_gotoloop();
	void func_gotonext();
	void func_gotoprevious();
	void func_play(Datum &frame, Datum &movie);
	void func_playdone();
	void func_cursor(Datum cursorDatum);
	int func_marker(int m);
	uint16 func_label(Datum &label);

	// lingo-the.cpp
public:
	void initTheEntities();
	void cleanUpTheEntities();
	const char *entity2str(int id);
	const char *field2str(int id);

	// global kTheEntity
	Datum _actorList;
	Common::u32char_type_t _itemDelimiter;
	bool _exitLock;
	bool _preLoadEventAbort; // no-op, everything is always preloaded
	Datum _searchPath;
	bool _trace;	// state of movie's trace function
	int _traceLoad; // internal Director verbosity level
	bool _updateMovieEnabled;
	bool _romanLingo;

	Datum getTheEntity(int entity, Datum &id, int field);
	void setTheEntity(int entity, Datum &id, int field, Datum &d);
	Datum getTheSprite(Datum &id, int field);
	void setTheSprite(Datum &id, int field, Datum &d);
	Datum getTheCast(Datum &id, int field);
	void setTheCast(Datum &id, int field, Datum &d);
	Datum getTheCastLib(Datum &id, int field);
	void setTheCastLib(Datum &id, int field, Datum &d);
	Datum getTheField(Datum &id1, int field);
	void setTheField(Datum &id1, int field, Datum &d);
	Datum getTheChunk(Datum &chunk, int field);
	void setTheChunk(Datum &chunk, int field, Datum &d);
	void getObjectProp(Datum &obj, Common::String &propName);
	void setObjectProp(Datum &obj, Common::String &propName, Datum &d);
	Datum getTheDate(int field);
	Datum getTheTime(int field);
	Datum getTheDeskTopRectList();

private:
	Common::StringArray _entityNames;
	Common::StringArray _fieldNames;

public:
	LingoCompiler *_compiler;
	LingoState *_state;

	int _currentChannelId;

	bool _freezeState;
	bool _freezePlay;
	bool _playDone;
	bool _abort;
	bool _expectError;
	bool _caughtError;

	TheEntityHash _theEntities;
	TheEntityFieldHash _theEntityFields;

	int _objectEntityId;

	SymbolHash _builtinCmds;
	SymbolHash _builtinFuncs;
	SymbolHash _builtinConsts;
	SymbolHash _builtinListHandlers;
	SymbolHash _methods;
	XLibOpenerFuncHash _xlibOpeners;
	XLibCloserFuncHash _xlibClosers;

	OpenXLibsHash _openXLibs;
	OpenXLibsStateHash _openXLibsState;
	Common::StringArray _openXtras;

	Common::String _floatPrecisionFormat;

public:
	void push(Datum d);
	Datum pop();
	Datum peek(uint offset);

public:
	Common::HashMap<uint32, const char *> _eventHandlerTypes;
	Common::HashMap<Common::String, uint32, Common::IgnoreCase_Hash, Common::IgnoreCase_EqualTo> _eventHandlerTypeIds;
	Common::HashMap<Common::String, Audio::AudioStream *> _audioAliases;

	DatumHash _globalvars;

	FuncHash _functions;

	Common::HashMap<int, const LingoV4Bytecode *> _lingoV4;
	Common::HashMap<int, const LingoV4TheEntity *> _lingoV4TheEntity;

	uint _globalCounter;

	DirectorEngine *_vm;

	int _floatPrecision;

	Datum _theResult;

	// events
	bool _passEvent;
	Datum _perFrameHook;

	Datum _windowList;
	Symbol _currentInputEvent;

	struct {
		LingoExecState _state = kRunning;
		bool (*_shouldPause)() = nullptr;
	} _exec;

public:
	void executeImmediateScripts(Frame *frame);
	void executePerFrameHook(int frame, int subframe);

	// lingo-utils.cpp
private:
	Common::HashMap<uint32, Common::U32String> _charNormalizations;
	void initCharNormalizations();

public:
	Common::String normalizeString(const Common::String &str);

public:
	void addBreakpoint(Breakpoint &bp);
	bool delBreakpoint(int id);
	Breakpoint *getBreakpoint(int id);

	const Common::Array<Breakpoint> &getBreakpoints() const { return _breakpoints; }
	Common::Array<Breakpoint> &getBreakpoints() { return _breakpoints; }

private:
	int _bpNextId = 1;
	Common::Array<Breakpoint> _breakpoints;
};

extern Lingo *g_lingo;

} // End of namespace Director

#endif
