(* [ʞ] rosshil.ml - tasshila semes sirvos
 *  ~ lexi hale <lexi@hale.su>
 *  $ ocamlopt.opt rosshil.ml -o rosshil
 *  © GNU EUPL v1.2
 *  ? rosshil is a tool for transforming various dates in the
 *    calendar systems of Spirals into other representations.
 *    feel free to copy it and use it for your own settings
 *    if you're as much of a perfectionist dweeb as me; just
 *    make sure you change the name of the utility so it's
 *    not in Ranuir
 *)

(* basic units *)
type timespan  = int (* earth seconds are current tick unit *)
type timepoint = int
type quantity = int
type proportion = float

type runopt = NoOpt | NoANSI | NoColor | ASCII | Debug | OutOnly
let optstrs = [
	NoANSI, "-plain";
	NoANSI, "-noansi";
	NoANSI, "p";

    NoColor, "-mono";
    NoColor, "-nocolor";
    NoColor, "-bw";
    NoColor, "m";

	ASCII, "-ascii";
	ASCII, "a";

    Debug, "-verbose";
    Debug, "-debug";
    Debug, "v";

    OutOnly, "-out";
    OutOnly, "o";
]

let is_opt str = List.exists (fun (_,s) -> ("-"^s) = str) optstrs
let opt_of_str str = let (opt,_) = List.find
	(fun (_,s) -> ("-"^s) = str) optstrs in opt
let in_opt_list opts o = List.exists (fun a -> a = o) opts 

type outfmt = {
    options: runopt list;
    ansi   : string -> string -> string;
    ul     : string -> string;
    hl     : string -> string;
    italic : string -> string;
    arrow  : string;
}

type name             = { short: string; long: string; }
let nmpair short long = { short = short; long = long   };

type timeunit = {
	name   : string;
	expr   : timespan -> string;
	span   : timespan;
}

let span (start: timepoint) (stop: timepoint) : timespan = stop - start
	(* because im a dyscalculic idiot *)

type epoch = {
	(* an epoch delineates discrete eras in a calendar system,
	 * such as which empress is reigning *)
	title : name;
	start : timepoint;
}
let inepoch (e: epoch) (date: timepoint) = date > e.start

type era = {
	(* an era is a period of time that may be attached to
	 * galactic history or to a single calendar *)
	title : name;
	start : timepoint;
	stop  : timepoint;
}
let eraspan (e: era) : timespan = span e.start e.stop

let history : era list = [] (* in DESCENDING order!! *)
let galacticzero : timepoint = 0

type date = { years : quantity; days : quantity }

let strify = string_of_int

exception Bad_dimension
let bad_dim _ = raise Bad_dimension

module Gregorian = struct
	module Span = struct
		let sec  : timespan =   1
		let min  : timespan =  60 * sec
		let hour : timespan =  60 * min
		let day  : timespan =  24 * hour
		let days : quantity = 365
		let year : timespan = days * day

		let equiv (d: date): timespan =
			(year * d.years) + (day * d.days)

		let day_equiv calendar_days earth_equiv = int_of_float
			(float day *. (float earth_equiv /. float calendar_days))

		let calc (span: timespan) : date =
			let ctdays = span / day in
			let ctyears = ctdays / days in
			let rem = ctdays mod days in
				{ years = ctyears; days = rem; }

		let fmt (d: date) : string = (
			(strify d.years) ^ " Gregorian years and " ^
			(strify d.days) ^ " days"
		)
	end
end

module Society = struct
	module Span = struct
		let days = 256
		let earth_days_in_year = 414
		let day = Gregorian.Span.day_equiv days earth_days_in_year
		let rantis = day / 64
		let year = day * days

		let equiv (d: date): timespan =
			(year * d.years) + (day * d.days)

		let calc (span: timespan) : date =
			let ctdays = span / day in
			let ctyears = ctdays / days in
			let rem = ctdays mod days in
				{years = ctyears; days = rem}

		let fmt (d : date) : string =
			("Sh." ^ (strify d.years) ^ "/" ^ (strify d.days))
	end

	module Cal = struct
		let zero = galacticzero + (0 * Span.year)

		let equiv (d: date): timepoint =
			zero + (Span.year * d.years) + (Span.day * d.days)

		let calc (pt: timepoint) : date =
			let adj = pt - zero in
			let span = Span.calc adj in
				{years = span.years; days = span.days; }

		let fmt (d: date) : string =
			("R" ^ (strify d.years) ^ "/" ^ (strify d.days))
	end
end

module Empire = struct
	module Span = struct
		let days = 376
		let earth_days_in_year = 394
		let day = Gregorian.Span.day_equiv days earth_days_in_year
		let year = day * days

		let equiv (d: date): timespan =
			(year * d.years) + (day * d.days)

		let calc (span: timespan) : date =
			let ctdays = span / day in
			let ctyears = ctdays / days in
			let rem = ctdays mod days in
				{years = ctyears; days = rem}

		let fmt (d : date) : string =
			((strify d.years)^" Old Carnelian orbital years, "^(strify d.days)^" days")
	end

	module Cal = struct
		let zero = Society.Cal.zero + (524 * Span.year)

		let equiv (d: date): timepoint =
			zero + (Span.year * d.years) + (Span.day * d.days)

		let calc (pt: timepoint) : date =
			let adj = pt - zero in
			let span = Span.calc adj in
				{years = span.years; days = span.days; }

		let fmt (d: date) : string =
			((strify d.years)^"ᵉ + "^(strify d.days)^" days")
	end
end

module League = struct
	module Span = struct
		let days = 300
		let earth_days_in_year = 388
		let day = Gregorian.Span.day_equiv days earth_days_in_year
		let year = day * days

		let equiv (d: date): timespan =
			(year * d.years) + (day * d.days)

		let calc (span: timespan) : date =
			let ctdays = span / day in
			let ctyears = ctdays / days in
			let rem = ctdays mod days in
				{years = ctyears; days = rem}

		let fmt (d : date) : string =
			((strify d.years)^" Topaz orbital years, "^(strify d.days)^" days")
	end

	module Cal = struct
		let zero = Society.Cal.zero + (928 * Society.Span.year)

		let equiv (d: date): timepoint =
			zero + (Span.year * d.years) + (Span.day * d.days)

		let calc (pt: timepoint) : date =
			let adj = pt - zero in
			let span = Span.calc adj in
				{years = span.years; days = span.days; }

		let fmt (d: date) : string =
			("L."^(strify d.years)^"."^(strify d.days))
	end
end

(*
let test () = let time : timepoint = Empire.Cal.equiv {years=953; days=38} in
	let span : timespan = time - galacticzero in
	List.iter print_string [
		"\x1b[1m"^ Gregorian.Span.(fmt(calc time)) ^" from Creation\x1b[0m\n";
		Society.Span.(fmt(calc span)) ^" from Creation →\n\t";
		Society.Cal.(fmt(calc time)) ^"\n";
		Empire.Span.(fmt(calc span)) ^" from Creation →\n\t";
		Empire.Cal.(fmt(calc time)) ^"\n";
		League.Span.(fmt(calc span)) ^" from Creation →\n\t";
		League.Cal.(fmt(calc time)) ^"\n";
	]
*)

type funcset = {
	calc : int -> date;
	fmt : date -> string;
	equiv : date -> int;
}

type funcrow = {
	names : string list;
	fmt : string;
	span : funcset;
	cal : funcset;
}


let funcs : funcrow list = [
	(* Society *) {
        names=["Society of Worlds"; "society"; "shil"; "ranshil"; "rs"];
		fmt="92";
		span = {
			calc=Society.Span.calc;
			fmt=Society.Span.fmt;
			equiv=Society.Span.equiv;
		};
		cal = {
			calc=Society.Cal.calc;
			fmt=Society.Cal.fmt;
			equiv=Society.Cal.equiv;
		};
	};
	(* Empire *) {
        names=["Heavenly Empire of a Thousand Suns"; "empire"; "imperial";"ziapha"; "zp"];
		fmt="91";
		span = {
			calc =Empire.Span.calc;
			fmt  =Empire.Span.fmt;
			equiv=Empire.Span.equiv;
		};
		cal = {
			calc   =Empire.Cal.calc;
			fmt    =Empire.Cal.fmt;
			equiv  =Empire.Cal.equiv;
		};
	};
	(* League *) {
        names=["Socialist League"; "league"; "ly"];
		fmt="96";
		span = {
			calc =League.Span.calc;
			fmt  =League.Span.fmt;
			equiv=League.Span.equiv;
		};
		cal = {
			calc   =League.Cal.calc;
			fmt    =League.Cal.fmt;
			equiv  =League.Cal.equiv;
		};
	};
	(* Gregorian *) {
        names=["Gregorian calendar"; "gregorian"; "terrestrial"; "earth"; "gy"; "ce";];
		fmt="95";
		span = {
			calc  = Gregorian.Span.calc;
			fmt   = Gregorian.Span.fmt;
			equiv = Gregorian.Span.equiv;
		};
		cal = {
			(* gregorian dates do not reference the same
			 * spacetime continuum that Spirals dates do,
			 * so any attempt to convert between calendars
			 * needs to raise an exception *)
			calc  = bad_dim;
			fmt   = bad_dim;
			equiv = bad_dim;
		};
	};
]


let usage (o: outfmt) bin = 
    let caltext = (
        let heading = "calendars: " in
        let desc (cal: funcrow) : string =(
            let name = List.hd cal.names in
            let aliases = List.tl cal.names in
            let append a b = if a = "" then b else (a^", "^b) in
            let fmt = cal.fmt in
                (o.ansi fmt (o.hl name)) ^
                (o.ansi fmt (" [" ^ (List.fold_left append "" aliases) ^"]") ^"\n"))
        in let spacing = String.make (String.length heading) ' ' in
        let space s = spacing ^ (desc s) in
        let fst = (o.hl heading) ^ (desc (List.hd funcs)) in
        let rst = List.map space (List.tl funcs) in
            (List.fold_left (^) fst rst)
    ) in

	let heading = "usage: " in
    let opts = " ["^(o.ul "options")^"] " in
    let param x = (if in_opt_list o.options NoANSI then "<"^x^">"
            else (o.hl (o.ul x))) in
	let syntax = [
		(param "calendar") ^ " [year "^(param "year")^"] [day "^(param "day")^"] to "^(param "calendar"),
			"convert a calendar date";
		(param "number") ^" "^ (param "calendar") ^ " (days | years ["^(param "days")^" days]) to "^(param "calendar"),
            "convert a span of time";
        (param "calendar")^" ticks "^(param "years")^" years "^(param "days")^" days",
            "count temporal ticks corresponding to a timespan";
        (param "calendar")^" epoch "^(param "year")^" day "^(param "day"),
            "get the Spirals epoch for a particular date";
        "epoch "^(param "number")^" to "^(param "calendar"),
            "convert a Spirals epoch "^(param "number")^" to a calendar date";
        (param "number")^" ticks to "^(param "calendar"),
            "convert a tick count to a span of time in a given calendar";
	] in
	let spacing = String.make (String.length heading) ' ' in
	let mkrow (s,d) = bin ^ opts ^ (o.ansi "34" s) ^"\n" ^
		spacing ^ ((o.ansi "95" (o.italic ("  "^o.arrow^" " ^ d)))^"\n") in
	let space s = spacing ^ (mkrow s) in
    let opt_heading = "options: " in
    let optdesc = [
        "o","out", "only print what the date is converted into";
        "p","plain", "do not format output with ansi escape codes";
        "m","mono", "emit monochrome output";
        "v","verbose", "output extra debugging information";
        "a","ascii", "use only ASCII in output (instead of UTF-8)";
    ] in
	let opt_spacing = String.make (String.length opt_heading) ' ' in
	let mkopt (s,l,d) = "-"^s^" "^(o.ansi "94" ("(--"^l^")"))^": "^(o.ansi "95" d)^"\n" in
	let spaceopt s = opt_spacing ^ (mkopt s) in

	let fst_opt = (o.hl opt_heading) ^ (mkopt (List.hd optdesc)) in
	let rst_opt = List.map spaceopt (List.tl optdesc) in
	let opt_body = List.fold_left (^) fst_opt rst_opt in

	let fst = (o.hl heading) ^ (mkrow (List.hd syntax)) in
	let rst = List.map space (List.tl syntax) in
        (List.fold_left (^) fst rst) ^"\n"^ opt_body ^"\n" ^ caltext;;


let contains ar v : bool = List.exists (fun a -> a = v) ar

let getfuncs (tag: string) : funcrow = List.find
	(* pred *) (fun (a:funcrow) -> contains a.names tag)
	(* from array *) funcs

type convkind = Calendar | Timespan

let conv (kind: convkind) tagfrom tagto years days =
	let choosefuncs system = match kind with
		| Calendar -> system.cal
		| Timespan -> system.span in
	let from_funcs = getfuncs tagfrom in
	let to_funcs = getfuncs tagto in
	let from_sys = choosefuncs from_funcs in
	let to_sys = choosefuncs to_funcs in
	let indate = {years = years; days = days} in
	let time = from_sys.equiv indate in
	let outdate = to_sys.calc time in
	let instr = from_sys.fmt indate in
	let outstr = to_sys.fmt outdate in
		(from_funcs.fmt, instr, to_funcs.fmt, outstr, time)


let rec parse (opts: runopt list) (args: string list) (bin: string) = let
	has_opt = in_opt_list opts in
	let switch o a b = if has_opt o then a else b in

	let o = {
        options = opts;
        ansi = (fun fmt str -> if has_opt NoANSI || has_opt NoColor then str else
            ("\x1b["^fmt^"m"^str^"\x1b[m"));
        italic = (fun txt -> switch NoANSI txt
            ("\x1b[3m" ^ txt ^ "\x1b[23m"));
        ul = (fun txt -> switch NoANSI txt
            ("\x1b[4m" ^ txt ^ "\x1b[24m"));
	    hl = (fun txt -> switch NoANSI txt
            ("\x1b[1m" ^ txt ^ "\x1b[21m"));
        arrow = (switch ASCII "->" "→");
    } in

    let dbg msg : unit = (if has_opt Debug then print_string
        (o.ansi "35" (o.italic ((o. hl " -- ") ^ msg)) ^ "\n") else ()) in
    let op msg o = dbg msg; o in
    
	let res_fmt (inf,ind,outf,outd,_) =
		(switch OutOnly "" ((o.ansi inf ind) ^" "^ o.arrow ^" "))^ (o.ansi outf (o.hl outd)) in
    let epoch (_,_,_,_,time) = time in

	match args with
	| opt :: rst when is_opt opt -> let o = opt_of_str opt in
        (op ("saw flag " ^opt^ ", adding to option set and restarting parse")
            parse (o :: opts) rst bin)

    (* convert timepoints between different calendars *)
	| [fromcal; "date"; date; "to"; calendar]
	| [fromcal; "year"; date; "to"; calendar] ->
		(res_fmt (conv Calendar fromcal calendar (int_of_string date) 0))^"\n"
	| [fromcal; "year"; years; "day"; days; "to"; calendar] ->
		(res_fmt (conv Calendar fromcal calendar
			(int_of_string years)
			(int_of_string days)))^"\n"

    (* absolute values - maybe useful for scripting, e.g.
     * comparing or sorting lists of dates in different
     * systems. *)
    | [cal; "epoch"; years; "day"; days;] ->
        (string_of_int (epoch (conv Calendar cal cal
            (int_of_string years)
			(int_of_string days))))^"\n"
    | [cal; "ticks"; years; "years"; days; "days"] ->
        (string_of_int (epoch (conv Timespan cal cal
            (int_of_string years)
			(int_of_string days))))^"\n"

    (* absolute values - convert timepoints/spans to dates *)
    | ["epoch"; epoch; "to"; system;] ->
        let fns = getfuncs system in
        (res_fmt ("3", "EPOCH "^epoch, fns.fmt,
            (fns.cal.fmt (fns.cal.calc (int_of_string epoch))), 0))^"\n"
    | [ticks; "ticks"; "to"; system;] ->
        let fns = getfuncs system in
        (res_fmt ("3", ticks^" TICKS", fns.fmt,
            (fns.span.fmt (fns.span.calc (int_of_string ticks))), 0))^"\n"

    (* get conversion factors between different days & years *)
	| [fromcal; "to"; calendar] ->
		(res_fmt (conv Timespan fromcal calendar 1 0))^"\n" ^
		(res_fmt (conv Timespan fromcal calendar 0 1))^"\n"

    (* convert timespans of one calendar system to another *) 
	| [count; from; "years"; "to"; system] ->
		(res_fmt (conv Timespan from system (int_of_string count) 0))^"\n"
	| [count; from; "days"; "to"; system] ->
		(res_fmt (conv Timespan from system 0 (int_of_string count)))^"\n"
	| [years; from; "years"; days; "days"; "to"; system] ->
		(res_fmt (conv Timespan from system
			(int_of_string years)
			(int_of_string days)))^"\n"
	| _ -> (usage o bin);;

let () = print_string (match (Array.to_list Sys.argv) with
	| bin :: rst -> parse [] rst bin
	| _ -> "buddy ya fucked up BAD");;
