#!/usr/bin/lua

--[[ foxclock version ᚢ.1 please read until the next 🐇 

foxclock is a fuzzy clock for fuzzy aminals. it uses a heretical take on the
ancient method of unequal, or seasonal, hours which was displaced by the
unceasing tide of standardization, where the daylight and the nightdark are each
divided into 7 sections (not the traditional 12), so that the length of a day
hour and a night hour are only the same on equinoxes. summerday hours are long
and lazy, winterday hours are fleeting and glittering.

the non-suncalc parts of this software were written by someone and their friend,
© a year of bnuuys. they are distributed under the Little Aminal Public License.
you are free to use, redistribute, modify, destroy, love, hate, pet, kiss, or
fuck the software ("the software") under the following conditia:

 - you agree ur cute soft little aminal in a big wide world
 - if ur plant/mycelium/rock/ocean/ghost/star/house/superorganism or something
   Else, that's cool too actually, don't worry about it.
 - u'll affirm your animality(/beingness) in some way today, such as by
    - sniffing at a berry
    - stretching your wings
    - feeling sunlight on your skin
    - feeling wind in your fur
    - feeling a tree looking back at you
 - you will help a worm or snail in their time of need
 - you will remember how sugar smells when it is poured into a bowl
 - you will know how it feels for a robin to startle you awake
 - you will think about what it would have been like to grow up by the sea
 - you will never be normal again
 - you will change
 - you will change
 - you will change
 - YOU WILL CHANGE
 - you will learn to tell the hours by looking at the light of the world
 - you will know this is a map and not the territory
 - you will redistribute this license along with the code
--]]

-- it is currently hardcoded to be in seattle. (its bring your own coordinates
-- go look 'em up urself) also if you use this above the arctic circle it will
-- probably break. if you have ideas for timekeeping in the arctic, reach out to
-- me by searching tirelessly for someone who keeps changing their name, their
-- face, their body, their language, and you lose hope you will ever know who i
-- am, until one day, you see me on a train and you somehow just Know that it is
-- Me, and you work up the courage to approach me but just as you clear your
-- throat, I get off the train, and you stand asweved in the twilight. or find
-- me on horny trans discord servers.
local lat = 47.6
local lng = -122.3
-- set this to false in the summer
local winter_mode=true
-- set this to true if you want a table of hours!!
local print_timetable_mode=false

-- change these to whatever you want. add as many seasons as you want. add
-- impossible seasons. add more hours, subtract more hours. add local seasons.
-- add dreaming-seasons. rearrange the hour names. you are alive in a world
-- where men no longer believe in unicorns and time is running out and all we
-- can do is love each other and make magic. 

-- foxclock will adjust fine if you make it have more or fewer hours.
local day_hour_names
local night_hour_names
if winter_mode then
	-- winter
	day_hour_names = {
		"halo",
		"steam",
		"pang",
		"mill",
		"glean",
		"gleam",
		"glance"
	}

	night_hour_names = {
		"gloam",
		"glow",
		"glim",
		"nill",
		"dream",
		"seem",
		"frost"
	}
else
	-- summer
	day_hour_names = {
		"dew",
		"stir",
		"snack",
		"mill",
		"sprawl",
		"brill",
		"honey"
	}
	night_hour_names = {
		"owl's light",
		"still",
		"snuck",
		"nill",
		"dream",
		"seem",
		"robin's light"
	}
end

-- change the prefix for the part of the hour
-- make them unfur. make them blossom. make them shake.
local Early_Name = "early"
local Mid_Name = "mid"
local Late_Name = "late"

-- 🐇 🐇 🐇 🐇 🐇 🐇 🐇 🐇 
-- you now no longer have to read the code unless you're using this as
-- a library then also have a look at the last few lines.

-- now begins Most Of Suncalc.Lua which we just copypasted into here --
-- Thanks suncalc.lua! --

--[[
Copyright (c) 2014, Vladimir Agafonkin
All rights reserved.

Redistribution and use in source and binary forms, with or 
without modification, are permitted provided that the following 
conditions are met:

   1. Redistributions of source code must retain the 
      above copyright notice, this list of conditions 
	  and the following disclaimer.

   2. Redistributions in binary form must reproduce the 
      above copyright notice, this list of conditions 
	  and the following disclaimer in the documentation 
	  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--]]

-- for simple translation of javascript ternary comparison operator ?:
local function ternary(a, b, c) if a then return b end return c end

-- shortcuts for easier to read formulas

local PI = math.pi
local sin = math.sin
local cos = math.cos
local tan = math.tan
local asin = math.asin
local atan = math.atan
local acos = math.acos
local rad = PI / 180

-- sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas


--  date/time constants and conversions

local dayS = 60 * 60 * 24
local J1970 = 2440588
local J2000 = 2451545

local function toJulian(date) return date / dayS - 0.5 + J1970 end
local function fromJulian(j)  return (j + 0.5 - J1970) * dayS end
local function toDays(date)   return toJulian(date) - J2000 end


-- general calculations for position

local e = rad * 23.4397 -- obliquity of the Earth

local function rightAscension(l, b) return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)) end
local function declination(l, b)    return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)) end

local function azimuth(H, phi, dec)  return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)) end
local function altitude(H, phi, dec) return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)) end

local function siderealTime(d, lw) return rad * (280.16 + 360.9856235 * d) - lw end

local function astroRefraction(h)
    if (h < 0) then -- the following formula works for positive altitudes only.
        h = 0 end   -- if h = -0.08901179 a div/0 would occur.

    -- formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998.
    -- 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad:
    return 0.0002967 / math.tan(h + 0.00312536 / (h + 0.08901179))
end

-- general sun calculations

local function solarMeanAnomaly(d) return rad * (357.5291 + 0.98560028 * d) end

local function eclipticLongitude(M)

    local C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)) -- equation of center
    local P = rad * 102.9372 -- perihelion of the Earth

    return M + C + P + PI
end

local function sunCoords(d)

    local M = solarMeanAnomaly(d)
    local L = eclipticLongitude(M)

    return {
        dec = declination(L, 0),
        ra = rightAscension(L, 0)
    }
end


local SunCalc = {}


-- calculates sun position for a given date and latitude/longitude

SunCalc.getPosition = function (date, lat, lng)

    local lw  = rad * -lng
    local phi = rad * lat
    local d   = toDays(date)

    local c  = sunCoords(d)
    local H  = siderealTime(d, lw) - c.ra

    return {
        azimuth = azimuth(H, phi, c.dec),
        altitude = altitude(H, phi, c.dec)
    }
end


-- sun times configuration (angle, morning name, evening name)

SunCalc.times = {
    {-0.833, 'sunrise',       'sunset'      },
    {  -0.3, 'sunriseEnd',    'sunsetStart' },
    {    -6, 'dawn',          'dusk'        },
    {   -12, 'nauticalDawn',  'nauticalDusk'},
    {   -18, 'nightEnd',      'night'       },
    {     6, 'goldenHourEnd', 'goldenHour'  }
}

-- adds a custom time to the times config

SunCalc.addTime = function (angle, riseName, setName)
    table.insert(SunCalc.times, {angle, riseName, setName})
end


-- calculations for sun times

local J0 = 0.0009

local function julianCycle(d, lw) return math.floor(0.5 + (d - J0 - lw / (2 * PI))) end

local function approxTransit(Ht, lw, n) return J0 + (Ht + lw) / (2 * PI) + n end
local function solarTransitJ(ds, M, L)  return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L) end

local function hourAngle(h, phi, d) return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))) end
local function observerAngle(height) return -2.076 * math.sqrt(height) / 60 end

-- returns set time for the given sun altitude
local function getSetJ(h, lw, phi, dec, n, M, L)

    local w = hourAngle(h, phi, dec)
    local a = approxTransit(w, lw, n)
    return solarTransitJ(a, M, L)
end


-- calculates sun times for a given date, latitude/longitude, and, optionally,
-- the observer height (in meters) relative to the horizon

SunCalc.getTimes = function (date, lat, lng, height, times)
    times = times or SunCalc.times
    height = height or 0

    local lw = rad * -lng
    local phi = rad * lat

    local dh = observerAngle(height)

    local d = toDays(date)
    local n = julianCycle(d, lw)
    local ds = approxTransit(0, lw, n)

    local M = solarMeanAnomaly(ds)
    local L = eclipticLongitude(M)
    local dec = declination(L, 0)

    local Jnoon = solarTransitJ(ds, M, L)

    local h0, Jset, Jrise


    local result = {
        solarNoon = fromJulian(Jnoon),
        nadir = fromJulian(Jnoon - 0.5)
    }

    for _, time in ipairs(times) do

        h0 = (time[01] + dh) * rad

        Jset = getSetJ(h0, lw, phi, dec, n, M, L)
        Jrise = Jnoon - (Jset - Jnoon)

        result[time[2]] = fromJulian(Jrise)
        result[time[3]] = fromJulian(Jset)
    end

    return result
end

-- 🦊 🦊 🦊 🦊 🦊
-- okay suncalc over now we are in the foxclock zone --

-- this is a utility function for printing out tables
local function tprint (tbl, indent)
    if not indent then indent = 0 end
    local toprint = string.rep(" ", indent) .. "{\r\n"
    indent = indent + 2 
    for k, v in pairs(tbl) do
      toprint = toprint .. string.rep(" ", indent)
      if (type(k) == "number") then
        toprint = toprint .. "[" .. k .. "] = "
      elseif (type(k) == "string") then
        toprint = toprint  .. k ..  "= "   
      end
      if (type(v) == "number") then
        toprint = toprint .. v .. ",\r\n"
      elseif (type(v) == "string") then
        toprint = toprint .. "\"" .. v .. "\",\r\n"
      elseif (type(v) == "table") then
        toprint = toprint .. tprint(v, indent + 2) .. ",\r\n"
      else
        toprint = toprint .. "\"" .. tostring(v) .. "\",\r\n"
      end
    end
    toprint = toprint .. string.rep(" ", indent-2) .. "}"
    print(toprint)
  end
  
local function hoursLater(date, h)
	-- dayS is a constant
    return date + h * dayS / 24
end


--[[
So let's say that time_list is a **list** of times, in time-order. Like this:

{
	{ -10, "sunset yesterday" },
	{  -5, "sunrise today" },
	{   5, "sunset today" },
	{  10, "sunrise tomorrow" }
}

And then say `now` is 0. We want to know the two times we are inbetween. So in
this case we would return "sunrise today", "sunset today" to show we are in the
day-part of today. That's what this function does.

You need to make sure that there are enough times here that we are in between
two of them. If not... well that'd be weird.
]]
local function when_even_are_we(now, time_list)
	for i in 1, #time_list - 1 do
		-- event time and name
		local etime_prev, ename_prev = table.unpack(time_list[i])
		local etime_next, ename_next = table.unpack(time_list[i + 1])

		if now >= etime_prev and now < etime_next then
			-- We are inbetween these two times
			return ename_prev, ename_next
		end
	end
end



--[[
This little friend will convert all our fun friendly hour names and say when
they happen in the silly little world of "24 hour" that the "humans" like to
use.
]]
local function print_hour_timetable(fuzziest_time, suntimes)
	local day_length = suntimes.sunset - suntimes.sunrise
	local night_length = (60 * 60 * 24) + suntimes.sunrise - suntimes.sunset

	-- We want to print the night first if its night, and the day first if its
	-- day. then we print the other.
	local first, second

	-- We start off with these in day, night order. But then if it's night we
	-- swap them. The start time just needs to be the start of *a* segment, it
	-- doesn't really need to be exactly today's segment because itll be close
	-- enough.
	first = {
		start = suntimes.sunrise,
		length = day_length,
		names = day_hour_names
	}
	second = {
		start = suntimes.sunset,
		length = night_length,
		names = night_hour_names
	}
	if fuzziest_time == 'night' then
		-- swap for night
		local tmp = first
		first = second
		second = tmp
	end

	-- Do the first, then the second.
	for _, seg in ipairs({first, second}) do
		-- How long is a hour?
		local hourlen = seg.length / #seg.names

		-- Now go through the segment, one hourlength at a time. Print out what
		-- 24-hour time it would be, and our hour name.
		for i = 1, #seg.names do
			local t = math.floor(seg.start + (i - 1) * hourlen)
			-- 24hour date, without the day

			-- For some reason lua5.4 on debian doesn't support %l in strftime,
			-- so we use %I which formats it with a leading 0, and then replace
			-- that with a space. lol
			local time_of_day_normalpeople = os.date('%I:%M %p', t):gsub('^0', ' ')
			local hour_name = seg.names[i]
			print(time_of_day_normalpeople .. ' - ' .. hour_name)
		end
	end
end

local function print_hour_timetable_for_date(date)
	-- HEY BUDDY YOU CANT BE COPY PASTING LAT/LNG LIKE THAT BUDDY
	-- probably somethings in the code should rearrange...
	local time = os.time(date)
	--[[
	local lat = 47.6
    local lng = -122.3 --]]
	local suntimes = SunCalc.getTimes(time, lat, lng)

	print_hour_timetable("day", suntimes)
end


--[[
For anything that wants to use us as a library, here's how they can do it! Any
function we put in "module" is a thing they can use
]]
local foxclock = {}

function foxclock.what_time_is_it(now)

    -- tprint(SunCalc.getPosition(now, lat, lng))
    -- tprint(SunCalc.getTimes(now, lat, lng))

	--[[
	This gives us the times of a bunch of interesting solar events, with their
	absolute timestamp in seconds since the epoch. Some of them will be before
	Now, and some of them will be after Now. Specifically, we have:
	- nadir
	- nightEnd
	- nauticalDawn
	- dawn
	- sunrise
	- sunriseEnd
	- goldenHour
	- goldenHourEnd
	- solarNoon
	- sunsetStart
	- sunset
	- dusk
	- nauticalDusk
	- night
	]]
    local suntimes = SunCalc.getTimes(now, lat, lng)

	--[[
	We want to know if it is day time or not time. If we are between sunrise and
	sunset for the day, then that means it's day time! Otherwise it is night. It
	is up to you, really, whether to use the start or end of the sunrise and
	sunset. but for now we will use `sunrise` and `sunset`
	]]
	local fuzziest_time
	local seg_start
	local seg_end
	if now > suntimes.sunrise and now < suntimes.sunset then
		-- DAY
		fuzziest_time = 'day'
		seg_start = suntimes.sunrise
		seg_end = suntimes.sunset
	else
		-- NIGHT
		fuzziest_time = 'night'
		--[[print(now)
		print(suntimes.sunrise)
		print(suntimes.sunset)
		--]]
		--[[
		If we are on the left of sunrise, then the segment ends at sunrise and
		starts at the *previous days* sunset. If we are on the right of sunset,
		then sunset is when the segment started and it will end on tomorrows
		sunrise.
		--]]
		if now < suntimes.sunrise then
			--[[
			For now, do the inaccurate but maybe close enough thing of taking
			24 hours off of today's number. This will be least accurate around
			the equinox and most accurate around the solstice.
			--]]
			seg_start = suntimes.sunset - 60 * 60 * 24
			seg_end = suntimes.sunrise
		else

			seg_start = suntimes.sunset
			seg_end = suntimes.sunrise + 60 * 60 * 24
			--print(seg_start)
			--print(seg_end)
		end
	end

	--[[
	Now we want to know how far through the current time-section we are. We want
	this as like a fraction out of 1.0, because we're going to re-scale it to
	our own fucked up time system :3 :3 :3
	]]
	local seg_progression = (now - seg_start) / (seg_end - seg_start)

	--[[
	now we re-scale it to ""hours"", which are some number of divisions of the
	segment. We will use the length of the names list to decide this, that way
	we don't need to remember to update both places when we make changes. Plus
	then you can have the day and night have different number of hours, and isnt
	that kinda neat?
	]]
	local num_hours
	if fuzziest_time == 'day' then
		num_hours = #day_hour_names
	else
		num_hours = #night_hour_names
	end
	local current_hour = math.floor(seg_progression * num_hours)
	--[[print("current hour")
	print(current_hour)--]]

	--[[
	If seg_progression is **exactly** 1.0, then current_hour can be num_hours.
	Which is weird. For a very small instant you might have that, if the maths
	work out that way. But that adds a secret num_hours + 1 hour which doesnt
	fit. So if current_hour == num_hours we should make it be num_hours - 1 for
	that instant.
	]]
	if current_hour == num_hours then
		current_hour = num_hours - 1
	end

	--[[
	But what is this hour named?? Let's find out! Don't forget to add 1, because
	current_hour from the maths we did will be from 0 to (num_hours - 1)
	]]
	local hour_name
	if fuzziest_time == 'day' then
		hour_name = day_hour_names[current_hour + 1]
	else
		hour_name = night_hour_names[current_hour + 1]
	end

	--[[ let's do math to find out how far we are within the hour ]]
	local hour_length = (seg_end - seg_start) / num_hours
	--[[]]
	local current_hour_start = seg_start + ((current_hour)*hour_length)
	local current_hour_progress = (now - current_hour_start) / hour_length
	--print(string.format("now %s current_hour %s seg_start %s seg_end %s hour_length %s current_hour_start %s current_hour_progress", now, current_hour, seg_start, seg_end, hour_length, current_hour_start, current_hour_progress))
	local minute_name
	if current_hour_progress < 0 then
		minute_name = "errneg"
	elseif current_hour_progress <= 0.33 then
		minute_name = Early_Name
	elseif current_hour_progress <= 0.66 then
		minute_name = Mid_Name
	elseif current_hour_progress <= 1 then
		minute_name = Late_Name
	else minute_name = "errpos"
	end
	--[[
	And now we know what time it is!
	]]

	if print_timetable_mode then
		print_hour_timetable("day", suntimes)
	end
	return string.format("%s %s", minute_name, hour_name)
end

local function mane()
    local now = os.time()

    print(foxclock.what_time_is_it(now))
end


-- swap these comments out to use this as a library (for instance, to put this on koreader)

-- return foxclock
mane()