-- Vegetable Macro for Tale 7 by thejanitor.
--
-- Thanks to veggies.lua for the build button locations
-- Updated 29-SEP-2017 by Silden to take into account UI changes that meant the windows would not close properly
-- Updated 30-SEP-2017 by Silden to increase default values to cater for long veg names, such as Cabbage

dofile("common.inc")
dofile("settings.inc")


WARNING=[[
THIS IS A BETA MACRO YOU ARE USING AT YOUR OWN RISK
You must be in the fully zoomed in top down F8 F8 F8 view, Alt+L to lock the camere once there.
In User Options -> Interface Options -> Menu You must DISABLE: "Right-Click Pins/Unpins a Menu"
You Must ENABLE: "Right-Click opens a Menu as Pinned"
You Must ENABLE: "Use the chat area instead of popups for many messages"
In Options -> One-Click and Related -> You must DISABLE: "Plant all crops where you stand"
In Options -> Video -> You must set: Shadow Quality and Time of Day lighting to the lowest possible.
Do not move once the macro is running and you must be standing on a tile with water available to refill.
Do not stand directly on or within planting distance of actual animated water.
]]

DEBUG=false

-- How many times to search for a plant / try open a bed window before giving up.
SEARCH_RETRYS=10

-- How many pixels in a NxN grid to look for before
MATCH_GRID_SIZE=5

-- These are the times in seconds it waits before watering a plant for a given stage.
-- For example, plant A is planted at time 0. At time 2.8 seconds the macro queues up plant A to be watered, it then
-- sleeps 0.2 seconds until FIRST_STAGE_WAIT time has passed before watering that plant an moving on.

-- If you encounter problems where plants are dieing at various stages it is ethier because these values are too low
-- causing a plant to be watered 3+ times in a single stage before it grows. Or it is because they are too high and
-- a plant is not recieving its water in time before regressing a stage.
-- Finally if in the final harvest stage you get getting messages about running out of water then probably it is trying
-- to harvest before it is ready and trying to rewater a plants 3rd stage. Increase the harvest wait to hopefully fix this.

-- TODO: Scale these based on global (and ideally local) teppy time.
FIRST_STAGE_WAIT = 4
SECOND_STAGE_WAIT = 28
THIRD_STAGE_WAIT = 41
HARVEST_STAGE_WAIT = 65

STAGE_WAITS = { FIRST_STAGE_WAIT, SECOND_STAGE_WAIT, THIRD_STAGE_WAIT, HARVEST_STAGE_WAIT }

-- How long to wait for the characters animations to stop at the end of each planting run. If this is too low
-- then instead of clicking a newly placed plant the macro will hit your character. So if you see at the start of a new
-- cycle the character menu being opened by the macro increase this value.
END_OF_RUN_WAIT = 3000

-- Controls the size of each search box. The larger this is the slower the search phase which can break everything.
SEARCH_BOX_SCALE = 1/10

MAX_PLANTS=12

RED = 0xFF2020ff
BLACK = 0x000000ff
WHITE = 0xFFFFFFff

LEEKS = "Horus' Grain"
SEED_NAMES = {
    "Tears of Sinai",
    "Green Leaf",
    "Bastet's Yielding",
    LEEKS,
    "Apep's Crop",
}
SEED_TYPES = {
    "Onions",
    "Carrots",
    "Cabbage",
    "Leeks",
    "Garlic",
}


-- Used to control the plant window placement and tiling.
WINDOW_HEIGHT=120 -- Was 80
WINDOW_WIDTH=240 -- Was 220
WINDOW_OFFSET_X=150
WINDOW_OFFSET_Y=150

function doit()
    while true do
        local config = makeReadOnly(getUserParams())
        askForWindowAndSetupGlobals(config)
        gatherVeggies(config)
    end
end

function askForWindowAndSetupGlobals(config)
    local min_jugs = config.num_waterings * config.num_plants * config.num_stages
    local min_seeds = config.num_plants + 8
    local one = 'You will need ' .. min_jugs .. ' jugs of water and at minimum ' .. min_seeds .. ' seeds \n'
    local two = '\n Press Shift over ATITD window to continue.'
    askForWindow(one .. two)
    setupGlobals()
end

function setupGlobals()
    NORTH   = Vector:new{0,-1}
    SOUTH   = Vector:new{0,1}
    WEST    = Vector:new{-1,0}
    EAST    = Vector:new{1,0}
    NORTH_WEST = NORTH + WEST
    NORTH_EAST = NORTH + EAST
    SOUTH_WEST = SOUTH + WEST
    SOUTH_EAST = SOUTH + EAST
    DOUBLE_SOUTH = SOUTH * 2
    DOUBLE_NORTH = NORTH * 2
    DOUBLE_WEST = WEST * 2
    DOUBLE_EAST = EAST * 2

    MOVE_BTNS = {}
    PLANT_LOCATIONS = {next=1}
    PlantLocation:new{direction_vector=NORTH, move_btn=Vector:new{59, 51}}
    PlantLocation:new{direction_vector=EAST ,move_btn=Vector:new{84, 74}}
    PlantLocation:new{direction_vector=SOUTH ,move_btn=Vector:new{60, 98}}
    PlantLocation:new{direction_vector=WEST ,move_btn=Vector:new{37, 75}}
    PlantLocation:new{direction_vector=NORTH, num_move_steps=2}
    PlantLocation:new{direction_vector=WEST, num_move_steps=2}
    PlantLocation:new{direction_vector=EAST, num_move_steps=2}
    PlantLocation:new{direction_vector=SOUTH, num_move_steps=2}
    PlantLocation:new{direction_vector=NORTH_EAST, move_btn=Vector:new{75, 62}}
    PlantLocation:new{direction_vector=NORTH_WEST, move_btn=Vector:new{45,60}}
    PlantLocation:new{direction_vector=SOUTH_EAST, move_btn=Vector:new{75, 91}}
    PlantLocation:new{direction_vector=SOUTH_WEST, move_btn=Vector:new{45, 87}}
    makeReadOnly(PLANT_LOCATIONS)

    local mid = getScreenMiddle()
    ANIMATION_BOX = makeBox(mid.x - 60, mid.y - 50, 105, 85)
    ARM_BOX = makeBox(mid.x - 90, mid.y - 20, 80, 25)
    BUILD_BTN = Vector:new{31, 135}
end

PlantLocation={}
function PlantLocation:new(o)
    if o.num_move_steps then
        o.move_btn = PLANT_LOCATIONS[o.direction_vector].move_btn
        o.direction_vector = o.direction_vector * o.num_move_steps
    else
        o.num_move_steps = 1
    end
    PLANT_LOCATIONS[o.direction_vector] = o
    PLANT_LOCATIONS[PLANT_LOCATIONS.next] = o
    PLANT_LOCATIONS.next = PLANT_LOCATIONS.next + 1
    o.box = makeSearchBox(o.direction_vector)
    return newObject(PlantLocation, o, true)
end

function PlantLocation:move()
    for step=1,self.num_move_steps do
        click(self.move_btn)
    end
end

function gatherVeggies(config)
    local plants = Plants:new{num_plants=config.num_plants }

    drawWater()
    for _=1,config.num_runs do
        local start = lsGetTimer()

        checkBreak()
        --lsSleep(3000)

        plants:iterate(Plant.plant, config)
        for round=1,config.num_stages+1 do
            plants:iterate(Plant.water, {stage_wait=STAGE_WAITS[round], num_waterings=config.num_waterings})
            checkBreak()
        end

        plants:iterate(Plant.close, config)

        lsSleep(click_delay)
        drawWater()
        lsSleep(click_delay*5)
        checkBreak()


        local stop = lsGetTimer() + END_OF_RUN_WAIT
        local total = math.floor((3600 / ((stop - start)/1000)) * config.num_plants * 9) -- default 3, currently 9 veggie yield with pyramids bonus
        sleepWithStatus(END_OF_RUN_WAIT, "Running at " .. total .. " veg per hour. ")
    end
end

-- Simple container object which constructs N plants and allows iteration over them.
Plants={}
function Plants:new(o)
    for index=1,o.num_plants do
        local location = PLANT_LOCATIONS[index]
        self[index] = Plant:new{index=index, location=location}
    end
    return newObject(self,o,true)
end

function Plants:iterate(func, args)
    for index=1,self.num_plants do
        func(self[index], args)
    end
end

Plant = {}
function Plant:new(o)
    o.window_pos = indexToWindowPos(o.index)
    return newObject(self,o)
end

function Plant:plant(config)
    -- Take of a snapshot of the area in which we are guessing the plant will be placed before we actually create
    -- and place it.
    if not self.saved_plant_location then
        lsSleep(click_delay)
        self.beforePlantPixels = getBoxPixels(self.location.box)
    end

    clickPlantButton(config.seed_name)
    self.location:move()
    local spot = getWaitSpotAt(BUILD_BTN)
    click(BUILD_BTN)
    self.plant_time = lsGetTimer()
    waitForChange(spot, click_delay*5)
    lsSleep(click_delay)

    if not self.saved_plant_location then
        for _=1,SEARCH_RETRYS do
            if self:searchForPlant() then
                break
            end
            lsPrintln("Search retry for plant " .. self.index)
            lsSleep(tick_delay)
        end
        if not self.saved_plant_location then
            lsPrintln("Fail search for plant" .. self.index)
        end

    end

    self:openBedWindow(config.alternate_drag)
end

function Plant:searchForPlant()
    lsPrintln("Searching for plant " .. self.index)
    return findChangedRow(self.location.box, self.beforePlantPixels,
        function (location, pixels)
            self.saved_plant_location = location
            self.saved_plant_pixels = pixels
        end
    )
end

function Plant:openBedWindow(alternate_drag)
    if not self.saved_plant_location then
        lsPrintln("No Saved location for plant " .. self.index)
        return
    end

    -- Wierd hacky thing, move the mouse to where the window will be and then safeClick the plant which causes
    -- the window to open instantly at the desired location and not where we clicked the plant.
    -- TODO: problably do something different as this is the only thing that takes mouse control from the user.

    self.window_open = false
    local window_spot = getWaitSpotAt(self.window_pos + {5,5})
    for _=1, SEARCH_RETRYS do
        if waitForPixelsAt(self.saved_plant_location, self.location.box, self.saved_plant_pixels) then
            if alternate_drag then
                local open_spot = getWaitSpotAt(self.saved_plant_location + {5,5})
                click(self.saved_plant_location,true,true)
                if waitForChange(open_spot,click_delay*5) then
                    lsSleep(click_delay)
                    drag(self.saved_plant_location.x+5,self.saved_plant_location.y+5,self.window_pos.x,self.window_pos.y,click_delay*2)
                end
            else
                moveMouse(self.window_pos)
                click(self.saved_plant_location ,1)
            end

            self.window_open = waitForChange(window_spot, click_delay*5)
            if self.window_open then
                break
            end
        end
        lsPrintln("Bed window open retry for plant " .. self.index)
        lsSleep(click_delay)
    end

    if not self.window_open then
        lsPrintln("Bed window open fail for plant " .. self.index)
    end
end

function waitForPixelsAt(vector, box, pixels)
    local match_size = MATCH_GRID_SIZE
    local half_match_size = math.floor(match_size/2)
    local box_location_x = vector.x - half_match_size
    local box_location_y = vector.y - half_match_size
    local match_box = makeBox(box_location_x, box_location_y, match_size, match_size)
    local all_same = true
    iterateBoxPixels(match_box, function(x,y,pixel)
        local pixels_x = box_location_x + x - box.left
        local pixels_y = box_location_y + y - box.top
        local old_pixel = pixels[pixels_y][pixels_x]
        local diff = calculatePixelDiffs(old_pixel, pixel)
        local diff_ok = diff[1] < 10 and diff[2] < 10 and diff[3] < 10
        all_same = all_same and diff_ok
    end)
    return all_same
end


-- For a given plants index sleep until time_seconds has passed for that plant since it was planted.
function Plant:sleepUntil(time_seconds)
    local sleepTime = time_seconds*1000 - (lsGetTimer() - self.plant_time);
    if sleepTime > 0 then
        sleepWithStatus(sleepTime, "Sleeping for " .. sleepTime)
    end
end

function Plant:clickWindow(offset,right_click,show)
    if self.window_open then
        click(self.window_pos + offset,right_click,show)
    end
end

function Plant:water(args)
    if not self.window_open then
        lsPrintln("Trying to water plant " .. self.index .. " which has no window open")
        return
    end

    checkBreak()
    self:sleepUntil(args.stage_wait)

    local search_box = makeBox(self.window_pos.x,self.window_pos.y-40,40,40)
    srReadScreen()
    local this_loc = srFindImageInRange("This.png", search_box.left, search_box.top, search_box.width, search_box.height, 4800)
    if not this_loc then
        lsPrintln("Didn't find This text for plant " .. self.index)
        self.window_open = false
        return
    end
    this_loc = Vector:new{x=this_loc[0],y=this_loc[1]+10}

    click(this_loc)
    for _=1, args.num_waterings do
        lsSleep(click_delay)
        click(this_loc+{0,25})
        click(this_loc)
        checkBreak()
    end
end

function Plant:close(config)
    -- New UI changes affected the closing of finished windows.
    -- Now look for the UnPin image, and safeClick it closed if found.
    
    local search_box = makeBox(self.window_pos.x+20,self.window_pos.y-50,220,80)
    srReadScreen()
    local unpin_loc = srFindImageInRange("UnPin.png", search_box.left, search_box.top, search_box.width, search_box.height, 4800)
    if unpin_loc then
	safeClick(unpin_loc[0],unpin_loc[1]);
    else
	lsPrintln("Didn't find unpin image for plant " .. self.index .. " looking left:" .. search_box.left .. ", top: " .. search_box.top .. ", width: " .. search_box.width .. ", height: " .. search_box.height .. ".");
        return
    end
end

-- Create a table of direction string -> box. Each box is where we will search the plant placed for that given direction
-- string.
-- Full of janky hardcoded values.
-- TODO: Make debuging this easier, figure out pixel scaling for different resolutions, get rid of magic numbers.
function makeSearchBox(direction)
    local xyWindowSize = srGetWindowSize()
    local search_size = math.floor(xyWindowSize[0] * SEARCH_BOX_SCALE)
    local mid = getScreenMiddle()
    local offset_mid = mid - {math.floor(search_size / 3), math.floor(search_size / 3) }

    local top_left = offset_mid + direction*40 - Vector:new{20,20 }

    local box = makeBox(top_left.x,top_left.y, search_size, search_size)
    box.direction = direction
    return box
end

function getScreenMiddle()
    local xyWindowSize = srGetWindowSize()
    return Vector:new{math.floor(xyWindowSize[0]/2), math.floor(xyWindowSize[1]/2)}
end

-- Tiling method from Cinganjehoi's original bash script. Tried out the automato common ones but they are slow
-- and broke sometimes? This is super simple and its not the end of the world if it breaks a little during a run.
function indexToWindowPos(index)
    local columns = getNumberWindowColumns()
    local x = WINDOW_WIDTH*((index-1) % columns) + WINDOW_OFFSET_X
    local y = WINDOW_HEIGHT*math.floor((index-1) / columns) + WINDOW_OFFSET_Y
    return Vector:new{x, y}
end

function getNumberWindowColumns()
    local xyWindowSize = srGetWindowSize()
    local width = xyWindowSize[0] * 0.6
    return math.floor(width / WINDOW_WIDTH);
end

function clickPlantButton(seed_name)
    local build_menu_opened = false
    while not build_menu_opened do
        local plantButton = findText(seed_name)
        if plantButton then
            local spot = getWaitSpotAt(Vector:new{5,5})
            clickText(plantButton, 1)
            build_menu_opened = waitForChange(spot,click_delay*5)
        else
	    lsPlaySound("fail.wav");
            error("Text " .. seed_name .. " Not found.")
        end
        sleepWithStatus(tick_delay, "Planting...") --Retrying build menu open
    end
end

function getBoxPixels(box)
    local pixels = {}
    iterateBoxPixels(box,
        function(x,y,pixel)
            pixels[y][x] = pixel
        end,
        function(y)
            pixels[y] = {}
        end
    )
    return pixels
end

-- Finds a row of pixels which have changed in the current ReadScreen buffer compared to a given 2d array of pixels.
function findChangedRow(box, pixels, func)
    if DEBUG then
        debugShowBox(box)
    end

    local changed = {}
    local new_pixels = {}
    iterateBoxPixels(box,
        function(x,y,pixel)
            changed[y][x] = pixels[y][x] ~= pixel
            new_pixels[y][x] = pixel
        end,
        function(y)
            changed[y] = {}
            new_pixels[y] = {}
        end
    )

    local search_from_left = box.direction.x <= 0
    local search_from_top = box.direction.y < 0

    local y_start, y_end, y_inc = 0, math.floor(box.height/MATCH_GRID_SIZE)-1, 1
    if not search_from_top then
        y_start, y_end, y_inc = y_end, y_start, -1
    end
    local x_start, x_end, x_inc = 0, math.floor(box.width/MATCH_GRID_SIZE)-1, 1
    if not search_from_left then
        x_start, x_end, x_inc = x_end, x_start, -1
    end

    for y=y_start,y_end,y_inc do
        for x=x_start,x_end,x_inc do
            local all_changed = true
            for j=0,MATCH_GRID_SIZE-1 do
                for k=0,MATCH_GRID_SIZE-1 do
                    local changed_x = x*MATCH_GRID_SIZE+k
                    local changed_y = y*MATCH_GRID_SIZE+j
                    all_changed = all_changed and changed[changed_y][changed_x]
                end
            end
            if all_changed then
                local grid_centre_x = x*MATCH_GRID_SIZE + math.floor(MATCH_GRID_SIZE/2)
                local grid_centre_y = y*MATCH_GRID_SIZE + math.floor(MATCH_GRID_SIZE/2)
                func(Vector:new{box.left + grid_centre_x,box.top + grid_centre_y},new_pixels)
                return true
            end
        end
    end
    return false
end

function debugShowBox(box)
    srSetMousePos(box.left, box.top)
    sleepWithStatus(2000, "TOP LEFT")
    srSetMousePos(box.right, box.bottom)
    sleepWithStatus(2000, "BOT RIGHT")
end

function inside(vector,box)
    local x, y = vector.x, vector.y
    return (x >= box.left and x <= box.right) and (y >= box.top and y <= box.bottom)
end

function distanceCentre(vector)
    local mid = getScreenMiddle()
    local delta = vector - mid

    local dx = math.pow(delta.x,2)
    local dy = math.pow(delta.y,2)

    return math.sqrt(dx + dy)
end

function iterateBoxPixels(box, xy_func, y_func)
    srReadScreen()


    for y=0,box.height,1 do
        if y_func then y_func(y) end
        for x=0, box.width do
            local pixel = srReadPixelFromBuffer(box.left + x, box.top + y)
            if xy_func(x,y,pixel) then
                return
            end
        end
        checkBreak()
    end
end


-- Used to place gui elements sucessively.
current_y = 0
-- How far off the left hand side to place gui elements.
X_PADDING = 5

function getUserParams()
    local is_done = false
    local got_user_params = false
    local use_custom = readSetting("use_custom",false)
    local config = {alternate_drag=readSetting("alternate_drag")}
    config.seed_name = readSetting("seed_name","")
    config.num_waterings = readSetting("num_waterings",1)
    config.num_stages = readSetting("num_stages",1)
    local seed_index = readSetting("seed_index",1)
    local display_seed_names = {}
    for i=1,5 do
        display_seed_names[i] = SEED_NAMES[i] .. " (" .. SEED_TYPES[i] .. ")"
    end
    while not is_done do
        current_y = 10

        if not got_user_params then
            local max_plants       = MAX_PLANTS
            use_custom = lsCheckBox(X_PADDING, current_y, 10, WHITE, "Use custom seed?", use_custom)
            current_y = 45
            if use_custom then
                config.seed_name     = drawEditBox("seed_name", "Custom seed name? ", config.seed_name, false)
                config.num_waterings = drawNumberEditBox("num_waterings", "How many waterings per stage? ", config.num_waterings)
                config.num_stages    = drawNumberEditBox("num_stages", "How many watering stages? ", config.num_stages)
            else
                seed_index         = lsDropdown("seed_name", X_PADDING, current_y, 10, lsScreenX - 10, seed_index, display_seed_names)
                current_y = 85
            end
            config.num_plants      = drawNumberEditBox("num_plants", "How many to plant per run? Max " .. max_plants, 12)
            config.num_runs        = drawNumberEditBox("num_runs", "How many runs? ", 20)
            config.click_delay     = drawNumberEditBox("click_delay", "What should the click delay be? ", 100)
            config.alternate_drag  = lsCheckBox(X_PADDING, current_y, 10, WHITE, "Alternate (slow) dragging?", config.alternate_drag)
            got_user_params = true
            for k,v in pairs(config) do
                got_user_params = got_user_params and v
            end
            got_user_params = got_user_params and drawBottomButton(lsScreenX - 5, "Next step")
        else
            drawWrappedText(WARNING, RED, X_PADDING, current_y)

            is_done = drawBottomButton(lsScreenX - 5, "Start Script")
        end

        if drawBottomButton(110, "Exit Script") then
            error "Script exited by user"
        end

        lsDoFrame()
        lsSleep(10)
    end

    writeSetting("seed_index",seed_index)
    writeSetting("alternate_drag",config.alternate_drag)
    writeSetting("use_custom",use_custom)
    writeSetting("seed_name",config.seed_name)
    writeSetting("num_waterings",config.num_waterings)
    writeSetting("num_stages",config.num_stages)
    config.num_plants = limitMaxPlants(config.num_plants)
    if not use_custom then
        config.seed_name = SEED_NAMES[seed_index]
        config.num_waterings = config.seed_name == LEEKS and 3 or 2
        config.num_stages = 3
    end
    click_delay = config.click_delay
    return config
end

function limitMaxPlants(user_supplied_max_num)
    return math.min(12, user_supplied_max_num)
end

function drawNumberEditBox(key, text, default)
    return drawEditBox(key, text, default, true)
end

function drawEditBox(key, text, default, validateNumber)
    drawTextUsingCurrent(text, WHITE)
    local width = validateNumber and 50 or 200
    local height = 30
    local done, result = lsEditBox(key, X_PADDING, current_y, 0, width, height, 1.0, 1.0, BLACK, default)
    if validateNumber then
        result = tonumber(result)
    elseif result == "" then
        result = false
    end
    if not result then
        local error = validateNumber and "Please enter a valid number!" or "Enter text!"
        drawText(error, RED, X_PADDING + width + 5, current_y + 5)
        result = false
    end
    current_y = current_y + 35
    return result
end

function drawTextUsingCurrent(text, colour)
    drawText(text, colour, X_PADDING, current_y)
    current_y = current_y + 20
end
function drawText(text, colour, x, y)
    lsPrint(x, y, 10, 0.7, 0.7, colour, text)
end

function drawWrappedText(text, colour, x, y)
    lsPrintWrapped(x, y, 10, lsScreenX-10, 0.6, 0.6, colour, text)
end

function drawBottomButton(xOffset, text)
    return lsButtonText(lsScreenX - xOffset, lsScreenY - 30, z, 100, WHITE, text)
end

-- Simple immutable vector class
Vector={}
function Vector:new(o)
    o.x = o.x or o[1]
    o.y = o.y or o[2]
    return newObject(self, o, true)
end

function Vector:__add(vector)
    local x,y = Vector.getXY(vector)
    return Vector:new{self.x + x, self.y + y}
end

function Vector:__sub(vector)
    local x,y = Vector.getXY(vector)
    return Vector:new{self.x - x, self.y - y}
end

function Vector:__div(divisor)
    return Vector:new{self.x / divisor, self.y / divisor}
end

function Vector:__mul(multiplicand)
    return Vector:new{self.x * multiplicand, self.y * multiplicand}
end

function Vector.getXY(vector)
    return vector.x or vector[1], vector.y or vector[2]
end

function Vector:length()
    return math.sqrt(self.x^2 + self.y^2)
end

function Vector:normalize()
    return self / self:length()
end

function Vector:__tostring()
    return "(" .. self.x .. ", " .. self.y .. ")"
end



function click(vector, right_click, show_mouse)
    if show_mouse then
        srClickMouse(vector.x, vector.y, right_click)
    else
        safeClick(vector.x, vector.y, right_click)
    end
    lsSleep(click_delay)
end

function moveMouse(vector)
    srSetMousePos(vector.x, vector.y)
    lsSleep(click_delay)
end

function getWaitSpotAt(vector)
    return getWaitSpot(vector.x, vector.y)
end

-- Helper function used in an objects constructor to setup its metatable correctly allowing for basic inheritence.
function newObject(class, o, read_only)
    o = o or {}
    setmetatable(o, class)
    class.__index = class
    if read_only then
        makeReadOnly(o)
    end
    return o
end

function makeReadOnly(table)
    local mt = getmetatable(table)
    if not mt then
        mt = {}
        if not table then print(debug.traceback()) end
        setmetatable(table,mt)
    end
    mt.__newindex = function(t,k,v)
        error("Attempt to update a read-only table", 2)
    end
    return table
end
