-- imports
dofile( "/utils/lib/item_info" )

-- defaults
loglevel = 1 -- trace = 6, ... , off = 0
attempt_limit = 10
fuel_warning = 50
fuel_critical = 10
default_transit_level = 80
log_dest = {terminal=true, loghost=true}
loghost = 3
max_try = 32

-- constants
orientations = {'s', 'w', 'n', 'e'}
orientations_verbose = {'south', 'west', 'north', 'east'}
verbose_levels = {'FATAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'TRACE'} 
known_loot = {}
inv = {nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil}
invc = {nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil} -- known counts of inventory

o2dx = {0, -1, 0, 1}
o2dz = {1, 0, -1, 0}

facings = {}
facings.s = 1
facings.south = 1
facings.w = 2
facings.west = 2
facings.n = 3
facings.north = 3
facings.e = 4
facings.east = 4

-- state

state = {}

state.x = 0
state.y = 0
state.z = 0
state.o = 1 -- orientation [1..4]
if turtle ~= nil then state.f = turtle.getFuelLevel() else state.f = 0 end
state.placemarks = {}
state.inv = {}
state.move_mode = 'stop' -- how to move handling obstacles 'stop', 'dig' perhaps even 'cirumvent' or 'push'
state.unknown_counter = 1
state.selected = 1

-- rednet.open('right')

--===#### Set logic ####===--

Set = {}
Set.mt = {}

function Set.new (t)
    local set = {}
    setmetatable(set, Set.mt)
    for _, l in ipairs(t) do set[l] = true end
    return set
end

Set.mt.__tostring = function (set)
    local s = '{'
    local sep = ''
    for e in pairs(set) do
        s = s .. sep .. e
        sep = ', '
    end
    return s .. '}'
end

Set.mt.__add = function (a,b)
    if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
        error('attempt to "unite" a set with a non-set value', 2)
    end
    local res = Set.new{}
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end

Set.mt.__mul = function (a,b)
    if getmetatable(a) ~= Set.mt or getmetatable(b) ~= Set.mt then
        error('attempt to "intersect" a set with a non-set value', 2)
    end
    local res = Set.new{}
    for k in pairs(a) do
        res[k] = b[k]
    end
    return res
end

Set.mt.__le = function (a,b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    return true
end

Set.mt.__lt = function (a,b)
    return a <= b and not (b <= a)
end

Set.mt.__eq = function (a,b)
    return a <= b and b <= a
end

Set.mt.__len = function (a)
    print('wibble')
    return  12
end

function Set.print (s)
  print(Set.tostring(s))
end

--===#### Wrappers ####===--

-- I want to be able to hook into every turtle api call

function getFuelLevel()
    state.f = turtle.getFuelLevel()
    return state.f
end

function check_fuel(auto_refuel)
    
end

function refuel(quantity)
    level_before = turtle.getFuelLevel()
    refueled = turtle.refuel(quantity)
    if refueled then
        level_after = getFuelLevel()
        gained_fuel = level_after - level_before
        debug('Got '..gained_fuel..' fuel')
        check_slot{slot=state.selected, gained_fuel=gained_fuel} -- update inventory + potentially refine knowledge
    end
    return refueled
end

function abstract_dig(dfun, options)
    options = options or {}
    assume = options.assume or nil
    dug = dfun()
    if dug then
        check_inventory(assume)
    end
    return dug
end

function dig(options)
    return abstract_dig(turtle.dig, options)
end

function digUp(options)
    return abstract_dig(turtle.digUp, options)
end

function digDown(options)
    return abstract_dig(turtle.digDown, options)
end

function abstract_place(pfun, item_or_slot)
    -- improve me please
    placed = pfun()
    return placed
end

function place(options)
    return abstract_place(turtle.place, options)
end

function placeUp(options)
    return abstract_place(turtle.placeUp, options)
end

function placeDown(options)
    return abstract_place(turtle.placeDown, options)
end

function craft()
    crafted = turtle.craft()
    if crafted then
        -- update inventory
    end
end

function select(slot)
    selected = turtle.select(slot)
    state.selected = slot -- probably should verify that a valid slot was selected, not slot -128 or something.
    return selected
end

function detect()
    return turtle.detect()
end

function detectUp()
    return turtle.detectUp()
end

function detectDown()
    return turtle.detectDown()
end

function abstract_compare(cfun, item_or_slot)
    --[[
        compare to a slot of known type
    ]]--
    slot = nil
    if type(item_or_slot) == 'number' then
        slot = item_or_slot
    elseif type(item_or_slot) == 'string' then
        for i=1,16 do
            if state.inv[i].item_type == item_or_slot then
                slot = i
                break
            end
        end
    end
    
    prev_slot = state.selected
    if slot ~= nil then turtle.select(slot) end
    result = cfun()
    turtle.select(prev_slot) -- might be more efficient conditionally?
    
    return result
end

function compare(item_or_slot)
    return abstract_compare(turtle.compare, item_or_slot)
end

function compareUp(item_or_slot)
    return abstract_compare(turtle.compareUp, item_or_slot)
end

function compareDown(item_or_slot)
    return abstract_compare(turtle.compareDown, item_or_slot)
end

--===#### Primitives ####===--

function dbg_location()
    debug('X:'..state.x..' Z:'..state.z..' Y:'..state.y..' O:'..orientations[state.o]..' F:'..state.f)
end

function print_table(t)

    for k,v in pairs(t) do
        print(tostring(k)..' : '..tostring(v))
    end

end

function init(options)
    options = options or {}
    state.x = options.x or 0 -- x coord
    state.y = options.y or 0 -- y coord
    state.z = options.z or 0 -- z coord
    state.o = norm_direction(options.o) or 1 -- orientation [1..4]
    state.f = options.f or turtle.getFuelLevel() -- fuel level
    state.inv = options.inv or {} -- inventory items + count
    state.move_mode = options.move_mode or 'stop' -- how to move handling obstacles 'stop', 'dig' perhaps even 'cirumvent' or 'push'  
end

function gps_init(level)
    print('trying to initialize')
    local x, y, z = gps.locate(30)
    -- local x, y, z = coords
    if x == nil then
        print("Couldn't get GPS location, returning nil")
        return false
    end
    
    print("I am at ",x," ",y," ",z)
    -- Now to determine my orientation
    while tx.detect() do
        tx.right()
    end
    -- should set the move mode to push in tx, but that's not yet implemented
    local moved = false
    print('moving one block to get my orientation')
    for j=1,max_try do
        if turtle.forward() then
            moved = true
            break
        else
            print('obstructing, trying again')
            os.sleep(1)
        end
    end
    if not moved then
        print('Failed to determine direction, returning nil')
        return false
    end
    local nx, ny, nz = gps.locate(30)
    print('I moved to '..nx..', '..ny..', '..nz)
    --local nx, ny, nz = ncoords
    -- move back, using turtle, cause back isn't implemented in tx
    moved = false
    for j=1,max_try do
        if turtle.back() then
            moved = true
            break
        else
            print('obstructing while moving back, trying again')
            os.sleep(1)
        end
    end
    -- go calculate heading
    if nx ~= x then
        print('x changed')
        if nx > x then 
            print('x increased')
            orientation = 'e'
        else
            print('x decreased')
            orientation = 'w'
        end
    else
        print('z must have changed')
        if nz > z then
            print('z increased')
            orientation = 's'
        else
            print('z decreased')
            orientation = 'n'
        end
    end
    print('I am facing '..orientation)
    tx.init{x=x, y=y, z=z, o=orientation, move_mode='dig'}
    tx.save_state()
    return true
end

--===#### Movement related  functions ####===--


function norm_direction(direction)
    -- return direction as a number
    nd = tonumber(direction)
    if nd and nd >= 1 and nd <=4 then return nd end
    nd = facings[direction]
    return nd
end

function left()
    turned = turtle.turnLeft()
    state.o = state.o - 1
    state.o = (state.o - 1) % 4
    state.o = state.o + 1
    return turned
end
turnLeft = left

function right()
    turned = turtle.turnRight()
    state.o = state.o + 1
    state.o = (state.o - 1) % 4
    state.o = state.o + 1
    return turned
end
turnRight = right

function face(direction)
    nd = norm_direction(direction)
    diff = state.o - nd
    if diff == 0 then
        return 
    elseif diff == -1 or diff == 3 then
        right()
    elseif diff == 1 or diff == -3 then 
        left()
    elseif math.random() > 0.5 then
        right()
        right()
    else
        left()
        left()
    end
end

function abstract_move(fmove, fdetect, fdig, dx, dy, dz, options)
    options = options or {}
    assume = options.assume
    -- if fdetect~= nil then obstruction = fdetect() end
    if not check_fuel() then return false end
    local attempt = 1
    while not fmove() do
        if state.move_mode == 'stop' then
            warn('Obstructed... :(')
            return false
        elseif state.move_mode == 'dig' then
            if attempt <= attempt_limit then
                dug = fdig()
                if dug then
                    check_inventory(assume)
                end
            else                
                warn('Undiggable... :(')
                return false
            end
        end
        attempt = attempt + 1
    end
    state.x = state.x + dx
    state.y = state.y + dy
    state.z = state.z + dz
    return true
end

function up(options)
    return abstract_move(turtle.up, turtle.detectUp, turtle.digUp, 0, 1, 0, options)
end

function down(options)
    return abstract_move(turtle.down, turtle.detectDown, turtle.digDown, 0, -1, 0, options)
end

function forward(options)
   return abstract_move(turtle.forward, turtle.detect, turtle.dig, o2dx[state.o], 0, o2dz[state.o], options)
end

function back(options)
   return abstract_move(turtle.back, nil, nil, -1*o2dx[state.o], 0, -1*o2dz[state.o], options)
end

function grid_move(options)
    --[[
        move turtle in a grid, 
    ]]--
    options = options or {}
    gx = options.gx
    gy = options.gy or 1
    gz = options.gz or 1
    dx = options.dx or 1
    dy = options.dy or 1
    dz = options.dz or 1
    hook = options.hook
    prefw_hook = options.prefw_hook
    assume = options.assume
    
    for iy=1,gy do
    
        for ix=1,gx do
            if hook then hook() end
            if ix < gx then
                for jx=1,dx do
                    if prefw_hook then prefw_hook() end
                    tx.forward{assume=assume}
                end
            end 
        end
        
        if iy < gy then
            if iy % 2 == 1 then
                fturn = tx.right
            else
                fturn = tx.left
            end
            fturn()
            for jy=1,dy do
                if prefw_hook then prefw_hook() end
                tx.forward{assume=assume}
            end
            fturn()
        end 
    
    end
    
end

function grid_test(options)
    --[[
        move turtle in a grid.
        please note that in this function the coordinates don't correspond to
        the squareworld coordinates.
        gx must always be positive and means the amount of grid node in front of the turtle
        negative gy => grid lays left of the turtle
        positive gy => grid lays right of the turtle
        positive gz implies the grid proceeds to above the turtle
        negative gz implies the grid proceeds to below the turtle
    ]]--
    options = options or {}
    gx = options.gx
    gy = options.gy or 1
    gz = options.gz or 1
    dx = options.dx or 1
    dy = options.dy or 1
    dz = options.dz or 1
    hook = options.hook
    intra_hook = options.intra_hook or nil
    assume = options.assume
    
    
    for iz=1,gz do
        for iy=1,gy do
            for ix=1,gx do
            
                if hook then hook() end
                if ix < gx then
                    for jx=1,dx do
                        tx.forward{assume=assume}
                    end
                end 
            end
            
            if iy < gy then
                if iy % 2 == 1 then
                    fturn = tx.right
                else
                    fturn = tx.left
                end
                fturn()
                for jy=1,dy do
                    tx.forward{assume=assume}
                end
                fturn()
            end 
            
        end
        
        -- go up or down
    end
end



--===#### Location related functions ####===--

function get_pos(with_orientation)
    -- return a table with the current coords
    bm = {}
    bm.x = state.x
    bm.y = state.y
    bm.z = state.z
    if with_orientation then
        bm.o = orientations[state.o]
    end
    return bm
end

function placemark(name, with_orientation)
    -- record a position and orientation for future reference as a placemark
    pos = get_pos(with_orientation)
    state.placemarks[name] = pos
    debug('placemarked position "'..name..'"')
    return pos
end

function get_placemarks()
    -- return a list of all recorded placemarks
    return state.placemarks
end

function goto_placemark(name)
    -- goto the position of the placemark represented by the given name
    pos = state.placemarks[name]
    if not pos then
        error('Placemark not found')
    end
    goto(pos.x, pos.y, pos.z, pos.o)
end

function goto_pos(pos, transit_level)
    goto(pos.x, pos.y, pos.z, pos.o, transit_level)
end

function goto(to_x, to_y, to_z, direction, transit_level)
    transit_level = transit_level or default_transit_level

    --print('Y: '..state.y..' -> '..to_y)
    if state.y < to_y then
        move_action = up
    else
        move_action = down
    end
    while state.y ~= to_y do
        moved = move_action()
        if moved == false and state.move_mode == 'stop' then return false end
    end
        
    --print('X: '..state.x..' -> '..to_x)
    if state.x < to_x then
        face('east')
        move_action = forward
    elseif state.x > to_x then
        face('west')
        move_action = forward
    end
    while state.x ~= to_x do
        moved = move_action()
        if moved == false and state.move_mode == 'stop' then return false end
    end
        
    --print('Z: '..state.z..' -> '..to_z)
    if state.z < to_z then
        face('south')
        move_action = forward
    elseif state.z > to_z then
        face('north')
        move_action = forward
    end
    while state.z ~= to_z do
        moved = move_action()
        if moved == false and state.move_mode == 'stop' then return false end
    end
    
    -- print('we have direction '..tostring(direction))
    if direction then
        face(direction)
    end    
end

function move(dx,dy,dz, direction)
    goto(tx.state.x + dx, tx.state.x + dx, tx.state.x + dx, direction)
end

--===#### Inventory related functions ####===--

--[[
(meh, complicated stuff...)
Inventory may change with functions:
  dig*, suck*, drop*, place*, attack*, refuel, craft
Additional information may be gained by:
  craft, getItemCount, getItemSpace, compare*, place*, transfer, refuel
As a player though I'll generally fuck up the knowledge of my turtle by
manually editing their inventories.


]]--

function abstract_suck(sfun, assume)
    sucked = sfun()
    if sucked then
        check_inventory(assume)
    end
    return sucked
end

function suck(assume)
    return abstract_suck(turtle.suck, assume)
end

function suckUp(assume)
    return abstract_suck(turtle.suckUp, assume)
end

function suckDown(assume)
    return abstract_suck(turtle.suckDown, assume)
end

function abstract_drop(dfun, count)
    droped = dfun(count)
    if droped then
        check_inventory()
    end
    return droped
end

function drop(count)
    return abstract_drop(turtle.drop, count)
end

function dropUp(count)
    return abstract_drop(turtle.dropUp, count)
end

function dropDown(count)
    return abstract_drop(turtle.dropDown, count)
end

function getItemCount(slot)
    return turtle.getItemCount(slot)
end

function getItemSpace(slot)
    return turtle.getItemSpace(slot)
end

function compareTo(slot)
    return turtle.compareTo(slot)
end

function check_fuel()
    local fuel_level = turtle.getFuelLevel()
    state.f = fuel_level
    if (fuel_level <= 0) then
        warn('Fatal: out of fuel...')
        return false
    elseif (fuel_level <= fuel_critical) then
        warn('Critical: fuel level is '..fuel_level)
    elseif (fuel_level <= fuel_warning and (fuel_level % fuel_critical) == 0) then
        warn('Warning: fuel level is'..fuel_level)
    end
    return true
end

function string.startswith(String,Start)
   return string.sub(String,1,string.len(Start))==Start
end

function string.endswith(String,End)
   return End=='' or string.sub(String,-string.len(End))==End
end

function reset_inventory()
    state.inv = {}
end

function assert_item_type(slot, item_type, force)
    -- label the items in slot as item_type, return false if this slot was already unambigous unless forced
    count = turtle.getItemCount(slot)
    space = turtle.getItemSpace(slot)
    if force == true or state.inv[slot] == nil or state.inv[slot].item_type == nil or string.startswith(state.inv[slot].item_type, 'Unknown ') or type(state.inv[slot].item_type) == 'table' then
        state.inv[slot] = { item_type=item_type, count=count, space=space }
    end
end

function check_slot(options)
    options = options or {}
    slot = options.slot or nil
    gained_fuel = options.gained_fuel or nil
    
    count = turtle.getItemCount(slot)
    space = turtle.getItemSpace(slot)
    
    state.inv[slot].count=count
    state.inv[slot].space=space
    if count == 0 then state.inv[slot].item_type = nil end
end

function check_inventory(assume)
    --[[ check everything in the inventory and refine knowledge using a given assumption
    
    wrt count there are the following possibilities:
    - count == 0 : clear
    - count was stable : noop
    - count decreased to >= 1 : update count
    - count increased from 0
    - count increased from >= 1
    
    
    ]]--
    
    assume = assume or 'Unknown '..tostring(state.unknown_counter)
    -- print(tostring(assume))
    
    for i=1,16 do
        count = turtle.getItemCount(i)
        space = turtle.getItemSpace(i)
        if count == 0 then
            -- debug('Empty slot '..i)
            -- Perhaps should figure out if something existed there and is now gone?
            state.inv[i] = { item_type=nil, count=count, space=space }
        elseif state.inv[i] == nil or state.inv[i].count == 0 then
            debug('New item in slot '..i)
            state.inv[i] = { item_type=assume, count=count, space=space }
            if type(assume) == 'string' and string.startswith(assume, 'Unknown ') then
                state.unknown_counter = state.unknown_counter + 1
                assume = 'Unknown '..tostring(state.unknown_counter)
            end
        elseif state.inv[i].count < count then
            --debug('Item increased in slot '..i)
            state.inv[i].count = count
            state.inv[i].space = space
            -- Can I refine my item_type?
            if type(assume) == 'string' and string.startswith(assume, 'Unknown ') then
                -- No I can't
            elseif  type(assume) == 'string' then
                -- I might if the current item_type is unknown or if it is contained in the current table
                -- Fuck it, set operations are too much hassle atm, just assume assume is correct
                debug('Refined knowledge (only string) about item in '..i)
                state.inv[i].item_type = assume
            elseif  type(assume) == 'table' and type(state.inv[i].item_type) == 'string' and string.startswith(state.inv[i].item_type, 'Unknown ') then
                --print(state.inv[i].item_type)
                debug('Refined knowledge (t>u) about item in '..i)
                state.inv[i].item_type = assume
            else 
                -- debug('Not refining knowledge about item in '..i)
            end
        elseif state.inv[i].count == count then
            -- debug('Item count stable')
        else
            debug('Unknown situation')
            --print(i)
            --print(textutils.serialize(state.inv[i]))       
        end
    end
end

function cross_reference()
    -- Compare all known things to unknown things and label then known if the same
    for i=1,16 do
        if tx.state.inv[i] ~= nil and tx.state.inv[i].item_type ~= nil and not string.startswith(state.inv[i].item_type, 'Unknown ') then
            for j=1,16 do
                if j~=i and tx.state.inv[j] ~= nil and tx.state.inv[i].item_type ~= nil and not string.startswith(state.inv[i].item_type, 'Unknown ') then
                end
            end
        end
    end
end

function print_inventory()
    for i=1,16 do
        if tx.state.inv[i] ~= nil then
            t = tx.state.inv[i].item_type
            if t == nil then
                t = 'nil'
            elseif type(t) == 'table' then
                t = table.concat(t,'|')
            end
            s = i..': t='..t..' c='..tx.state.inv[i].count..' s='..tx.state.inv[i].space
            if tx.state.inv[i].count > 0 then
                print(s)
            end
        end
    end
end

--===#### Debug ####===--

function info()
    print('x,y,z,o,f: '..state.x..','..state.y..','..state.z..','..orientations[state.o]..','..tostring(state.f))
    print('mode: '..state.move_mode)    
end

function log(msg, lvl)
    time = textutils.formatTime(os.time(), true)
    verbose_level = verbose_levels[lvl]
    if loglevel >= lvl then
        if log_dest['terminal'] then
            print(time..' '..verbose_level..': '..msg)
        end
        if log_dest['loghost'] then
            rednet.send(loghost, verbose_level..': '..msg)
        end
    end
end

function debug(msg)
    log(msg, 5)
end

function warn(msg)
    log(msg, 3)
end

--===#### State related functions ####===--

function save_state()
    -- debug('Saving state...')
    f = fs.open('/tx.state', 'w')
    f.write(textutils.serialize(state))
    f.close()
    debug('State saved')
end

saves = save_state

function load_state()
    -- debug('Loading state...')
    if fs.exists('/tx.state') then
        f = fs.open('/tx.state','r')
        data = textutils.unserialize(f.readAll())
        f.close()
        -- Seems I need to copy data into state instead of loading directly into state
        for key,value in pairs(data) do
            state[key] = value
        end        
        debug('State loaded')
    else
        debug('No tx.state file found')
    end	
end
loads = load_state
