from PIL import Image
import numpy as np
import cv2
from enum import IntEnum
from pathlib import Path
from collections import namedtuple

from world_rando.coord import Coord, Rect
from world_rando.util import pairwise
from data_types.item_set import ItemSet, item_mapping

Infinity = float("inf")
TERMINAL_VELOCITY = 1
# Arbitrary
MAX_VERTICAL_SPEED = -15

#TODO: move from "is" to ==

# Part 1: Enumerated types
class AbstractTile(IntEnum):
    UNKNOWN = 0
    AIR = 1
    SOLID = 2
    GRAPPLE = 3
    BLOCK_BOMB = 4
    BLOCK_MISSILE = 5
    BLOCK_SUPER = 6
    BLOCK_POWER_BOMB = 7
    BLOCK_GRAPPLE = 8
    BLOCK_SPEED = 9
    BLOCK_CRUMBLE = 10
    BLOCK_SHOT = 11
    DOOR = 12

# Translation between tile colors and types of tile
unknown_color = (255, 255, 255)
player_before_color = (255, 0, 0)
player_after_color = (0, 255, 0)
air_color = (251, 242, 54)
water_color = (48, 185, 211)
solid_color = (0, 0, 0)
grapple_color = (119, 33, 105)
water_air_color = (30, 211, 106)
# Indicate water/air ambiguity AND player presence
player_before_water_air_color = (198, 0, 0)
player_before_water_color = (145, 0, 0)
player_after_water_air_color = (0, 198, 0)
player_after_water_color = (0, 145, 0)
item_color = (255, 22, 169)
door_color = (255, 22, 169)
# Destructible blocks
block_bomb_color = (141, 73, 154)
block_missile_color = (154, 73, 73)
block_power_bomb_color = (154, 116, 73)
block_super_color = (73, 154, 82)
block_grapple_color = (73, 150, 154)
block_speed_color = (73, 90, 154)
block_crumble_color = (107, 154, 73)
block_shot_color = (154, 150, 73)

abstract_to_color = {
    AbstractTile.UNKNOWN : unknown_color,
    AbstractTile.AIR : air_color,
    AbstractTile.SOLID : solid_color,
    AbstractTile.GRAPPLE : grapple_color,
    AbstractTile.BLOCK_BOMB : block_bomb_color,
    AbstractTile.BLOCK_MISSILE : block_missile_color,
    AbstractTile.BLOCK_SUPER : block_super_color,
    AbstractTile.BLOCK_POWER_BOMB : block_power_bomb_color,
    AbstractTile.BLOCK_GRAPPLE : block_grapple_color,
    AbstractTile.BLOCK_SPEED : block_speed_color,
    AbstractTile.BLOCK_CRUMBLE : block_crumble_color,
    AbstractTile.BLOCK_SHOT : block_shot_color,
    AbstractTile.DOOR : door_color
    }

abstract_to_image = {
    AbstractTile.UNKNOWN : "",
    AbstractTile.AIR : "tile_air.png",
    AbstractTile.SOLID : "tile_solid.png",
    AbstractTile.GRAPPLE : "tile_grapple.png",
    AbstractTile.BLOCK_BOMB : "tile_bomb.png",
    AbstractTile.BLOCK_MISSILE : "tile_missile.png",
    AbstractTile.BLOCK_SUPER : "tile_super.png",
    AbstractTile.BLOCK_POWER_BOMB : "tile_power_bomb.png",
    AbstractTile.BLOCK_GRAPPLE : "tile_grapple_crumble.png",
    AbstractTile.BLOCK_SPEED : "tile_speed.png",
    AbstractTile.BLOCK_CRUMBLE : "tile_crumble.png",
    AbstractTile.BLOCK_SHOT : "tile_shot.png",
    #TODO
    AbstractTile.DOOR : "tile_air.png"
    }

def to_conj(it):
    it2 = [i for i in it if len(i) > 0]
    if len(it2) == 0:
        return ""
    if len(it2) == 1:
        return it2[0]
    return "(" + " & ".join(it2) + ")"

def to_disj(it):
    it2 = [i for i in it if len(i) > 0]
    if len(it2) == 0:
        return ""
    if len(it2) == 1:
        return it2[0]
    return "(" + " | ".join(it2) + ")"

def itemset_as_tla(itemset, when="prev"):
    assert when == "prev" or when == "next"
    if len(itemset) == 0:
        return ""
    cs = []
    for i in itemset:
        clause = f"{i}_prev = 1"
        cs.append(clause)
    return to_conj(cs)

class SamusPose(IntEnum):
    STAND = 0
    MORPH = 1
    JUMP = 2
    SPIN = 3

class LiquidType(IntEnum):
    NONE = 0
    WATER = 1
    LAVA = 2
    ACID = 3

class VType(IntEnum):
    # Special "untyped" zero velocity
    RUN = 1
    SPEED = 2
    WATER = 3

# The number of tiles to run when you reach maximum horizontal velocity
velocity_maxima = {
    VType.RUN: 4,
    VType.SPEED: 30,
    VType.WATER: 5
        }

class VBehavior(IntEnum):
    STORE = 0
    LOSE = 1

class Interval(object):

    def __init__(self, left, right):
        assert left <= right
        self.left = left
        self.right = right

    def __contains__(self, n):
        return self.left <= n and n <= self.right

    def flip(self):
        """ Horizontally flip """
        return Interval(-self.right, -self.left)

    @property
    def size(self):
        return self.right - self.left

    def shift(self, n):
        return Interval(self.left + n, self.right + n)

    def interval_image(self, n, behavior):
        if behavior == VBehavior.LOSE:
            return Interval(n, n)
        elif behavior == VBehavior.STORE:
            return self.shift(n)
        else:
            assert False

    def __eq__(self, other):
        return self.left == other.left and self.right == other.right

    def subset(self, other):
        l = self.l >= other.l
        r = self.r <= other.r
        return l and r

    def intersect(self, other):
        l = max(self.left, other.left)
        r = min(self.right, other.right)
        # Set may be empty
        if l > r:
            return None
        else:
            return Interval(l, r)

    def horizontal_flip(self):
        return Interval(-self.right, -self.left)

    def copy(self):
        return Interval(self.left, self.right)

    def __repr__(self):
        return "i({}, {})".format(self.left, self.right)

class HVelocitySet(object):

    def __init__(self, types, interval):
        self.types = types
        self.interval = interval

    def __contains__(self, hvel):
        if hvel.type in self.types:
            return hvel.value in self.interval
        else:
            return hvel.value == 0 and 0 in self.interval

    def shift(self, hvel):
        if hvel.type in types or hvel.value == 0:
            i = self.interval.shift(hvel.value)
            return HVelocitySet(self.types, i)
        # Inherit the type if the velocity is only zero
        elif self.interval == Interval(0,0):
            return HVelocitySet(frozenset([hvel.type]), Interval(hvel.value, hvel.value))
        else:
            return None

    def interval_image(self, vh, behavior):
        if behavior == VBehavior.LOSE:
            return HVelocitySet(frozenset([vh.type]), Interval(vh.value, vh.value))
        elif behavior == VBehavior.STORE:
            return self.shift(vh)
        else:
            assert False

    def subset(self, other):
        # Type doesn't matter if the interval covers only zero
        if self.types <= other.types or self.interval == Interval(0,0):
            return self.interval.subset(other.interval)
        else:
            return False

    def intersect(self, other):
        i = self.interval.intersect(other.interval)
        t = self.t & other.t
        if i is None or len(t) == 0:
            return None
        else:
            return HVelocitySet(t, i)

    def horizontal_flip(self):
        t = self.types.copy()
        return HVelocitySet(t, self.interval.horizontal_flip())

class VelocitySet(object):

    def __init__(self, vertical_interval, horizontal_set):
        self.vertical_set = vertical_interval
        self.horizontal_set = horizontal_set

    def __contains__(self, velocity):
        v = velocity.vv in self.vertical_set
        h = velocity.vh in self.horizontal_set
        return v and h

    def as_tla(self, when="prev"):
        clauses = []
        assert (when == "prev" or when == "next")
        vvset = self.vertical_set
        vhset = self.horizontal_set.interval
        vvmin = vvset.left
        if vvmin < MAX_VERTICAL_SPEED:
            vvmin = MAX_VERTICAL_SPEED
        vvmax = vvset.right
        if vvmax > TERMINAL_VELOCITY:
            vvmax = TERMINAL_VELOCITY
        vhcap = int(max(velocity_maxima.values()))
        vhmin = vhset.left
        if vhmin < -vhcap:
            vhmin = -vhcap
        vhmax = vhset.right
        if vhmax > vhcap:
            vhmax = vhcap
        vvc1 = f"vv_{when} >= {vvmin}"
        vvc2 = f"vv_{when} <= {vvmax}"
        vhc1 = f"vh_{when} >= {vhmin}"
        vhc2 = f"vh_{when} <= {vhmax}"
        type_clauses = []
        for t in self.horizontal_set.types:
            tc = f"vt_{when} = {t}"
            type_clauses.append(tc)
        type_clause = " | ".join(type_clauses)
        type_clause = f"({type_clause})"
        clauses = [vvc1, vvc2, vhc1, vhc2, type_clause]
        return to_conj(clauses)

    def shift(self, velocity):
        v = vertical_set.shift(velocity.vv)
        h = horizontal_set.shift(velocity.vh)
        assert h is not None
        return VelocitySet(v,h)

    def subset(self, other):
        v = self.vertical_set.subset(other.vertical_set)
        h = self.horizontal_set.subset(other.horizontal_set)
        return v and h

    def intersect(self, other):
        v = self.vertical_set.intersect(other.vertical_set)
        h = self.horizontal_set.intersect(other.horizontal_set)
        if v is None or h is None:
            return None
        else:
            return VelocitySet(v, h)

    def horizontal_flip(self):
        return VelocitySet(self.vertical_set.copy(), self.horizontal_set.horizontal_flip())

VClauses = namedtuple("VClauses", ["domain", "vv", "vh", "vt"])

class VelocityFunction(object):

    def __init__(self, domain, vertical_b, horizontal_b, vadd):
        self.domain = domain
        self.vertical_behavior = vertical_b
        self.horizontal_behavior = horizontal_b
        self.vadd = vadd

    def apply(self, velocity):
        """ Apply to get a new velocity. None if the parameter is not in the domain """
        if velocity in self.domain:
            start_vel = velocity.copy()
            if self.vertical_behavior == VBehavior.LOSE:
                start_vel.vv = 0
            if self.horizontal_behavior == VBehavior.LOSE:
                start_vel.vh = HVelocity(VType.RUN, 0)
            return start_vel + self.vadd
        else:
            return None

    def as_tla(self):
        # vv is vertical velocity
        # vh is horizontal velocity
        # vt is the TYPE of horizontal velocity
        # DOMAIN
        # Conditions on vv_prev, vh_prev, and vt_prev
        domain_clause = self.domain.as_tla()
        domain_clause = f"({domain_clause})"

        # RANGE
        # Conditions on vv_next, vh_next, and vt_next
        start_vv = "vv_prev"
        start_vh = "vh_prev"
        start_vt = "vt_prev"
        if self.vertical_behavior == VBehavior.LOSE:
            start_vv = "0"
        if self.horizontal_behavior == VBehavior.LOSE:
            vh_max = velocity_maxima[VType.RUN]
            start_vh = "0"
            start_vt = f"{VType.RUN}"
        else:
            vh_max = max([velocity_maxima[t] for t in self.domain.horizontal_set.types])
        # Vertical clause
        vv_clause = f"vv_next = {start_vv} + {self.vadd.vv}"
        # Handle terminal velocity. vv_next will be either vv_prev + vadd, or whatever the max (or min) is
        if self.vadd.vv > 0:
            vv_clause_p2 = f"vv_prev = {TERMINAL_VELOCITY} & vv_next = {TERMINAL_VELOCITY}"
        elif self.vadd.vv < 0:
            vv_clause_p2 = f"vv_prev = {MAX_VERTICAL_SPEED} & vv_next = {MAX_VERTICAL_SPEED}"
        else:
            vv_clause_p2 = ""
        vv_clause = to_disj([vv_clause, vv_clause_p2])
        # Horizontal clause
        vh_clause = f"vh_next = {start_vh} + {self.vadd.vh.value}"
        # Terminal velocity for vh.
        if self.vadd.vh.value > 0:
            velocity_maxima
            vh_clause_p2 = f"vh_prev = {vh_max} & vv_next = {vh_max}"
        elif self.vadd.vh.value < 0:
            vh_clause_p2 = f"vh_prev = -{vh_max} & vv_next = -{vh_max}"
        else:
            vh_clause_p2 = ""
        vh_clause = to_disj([vh_clause, vh_clause_p2])
        # Type clause
        vt_clause = f"vt_next = {start_vt}"
        clauses = VClauses(domain_clause, vv_clause, vh_clause, vt_clause)
        return clauses

    @property
    def image(self):
        """ Velocity set which is the image """
        v_interval = self.domain.vertical_set.interval_image(self.vadd.vv, self.vertical_behavior)
        h_interval = self.domain.horizontal_set.interval_image(self.vadd.vh, self.horizontal_behavior)
        return VelocitySet(v_interval, h_interval)

    def preimage(self, vset):
        """ Pre-image of a set under this function """
        assert vset.subset(self.image)
        if self.vertical_behavior == VBehavior.LOSE:
            v_interval = Interval(-Infinity, Infinity)
        else:
            # Going backwards
            v_interval = self.domain.vertical_set.shift(-self.vadd.vv)
        if self.horizontal_behavior == VBehavior.LOSE:
            h_i = Interval(-Infinity, Infinity)
            h_set = frozenset([VType.RUN, VType.SPEED, VType.WATER])
            h_interval = HVelocitySet(h_i, h_set)
        else:
            h_interval = self.domain.horizontal_set.shift(-self.vadd.vh)
        return VelocitySet(v_interval, h_interval)

    def compose(self, other):
        # f x f'
        # The points that f maps to and f' maps from
        agree = self.image.intersect(other.domain)
        # Incompatible functions agree on no points
        if agree is None:
            return None
        # Points that can be mapped from
        p = self.preimage(agree)
        # Points that can be mapped from and are feasible
        domain = p.intersect(self.domain)
        # Incompatible if no points can be mapped from
        if domain is None:
            return None
        vh, hb = combine_vs(self.vadd.vh, other.vadd.vh, self.horizontal_behavior, other.horizontal_behavior)
        vv, vb = combine_vs(self.vadd.vv, other.vadd.vv, self.vertical_behavior, other.vertical_behavior)
        v = Velocity(vv, vh)
        return VelocityFunction(domain, vb, hb, v)

    def horizontal_flip(self):
        domainf = self.domain.horizontal_flip()
        addf = self.vadd.horizontal_flip()
        return VelocityFunction(domainf, self.vertical_behavior, self.horizontal_behavior, addf)

def combine_vs(v1, v2, b1, b2):
    """
    Combine velocities to obtain the result of doing both 'velocity actions'.
    """
    # Neither lose means add both
    if b1 != VBehavior.LOSE and b2 != VBehavior.LOSE:
        v = v1 + v2
        b = VBehavior.STORE
    # Second lose means it comes after applying the first,
    # So the intermediate velocity does not matter
    elif b2 is VBehavior.LOSE:
        v = v2
        b = VBehavior.LOSE
    # First lose and NOT second lose means the velocity change from v1 will carry over
    # and combine with v2
    elif b1 == VBehavior.LOSE:
        v = v1 + v2
        b = VBehavior.LOSE
    return v, b

class HVelocity(object):

    def __init__(self, vtype, value):
        self.type = vtype
        assert vtype in velocity_maxima
        self.value = value

    def __eq__(self, other):
        # Always must share value
        if self.value == other.value:
            # Type matters unless velocity is zero
            if self.type == other.type or self.value == 0:
                return True
        return False

    def __add__(self, other):
        if self.type == other.type:
            t = self.type
        # Inherit type if value is zero
        elif self.value == 0:
            t = other.type
        elif other.value == 0:
            t = self.type
        # Type mismatch is bad
        else:
            return None
        v = self.value + other.value
        vmax = velocity_maxima[t]
        if v > vmax:
            v = vmax
        elif v < -vmax:
            v = -vmax
        return HVelocity(t, v)

    def __hash__(self):
        return hash((self.type, self.value))

    def flip(self):
        return HVelocity(self.type, -self.value)

    def copy(self):
        return HVelocity(self.type, self.value)

#TODO: what about "underwater" vertical velocity vs. air-based vertical velocity?
#TODO: what about max velocity?
class Velocity(object):

    def __init__(self, vv, vh):
        # Integer
        self.vv = vv
        # (VType, Integer)
        self.vh = vh

    def horizontal_flip(self):
        return Velocity(self.vv, self.vh.flip())

    def __eq__(self, other):
        return self.vv == other.vv and self.vh == other.vh

    def __add__(self, other):
        v = self.vv + other.vv
        if v > TERMINAL_VELOCITY:
            v = TERMINAL_VELOCITY
        h = self.vh + other.vh
        assert h is not None
        return Velocity(v, h)

    def __hash__(self):
        return hash((self.vv, self.vh))

    def copy(self):
        hv = self.vh.copy()
        return Velocity(self.vv, hv)

    def __repr__(self):
        return "V: {}, H: {}, {}".format(self.vv, self.vh.type.name, self.vh.value)

#TODO: to "break" a block, need a set of {pose, item}.
# e.g. to break bomb blocks, need either bombs+mb, pbs+mb (and any pose), or screw + spin
# OR can be a speed! To break speed blocks, need max speedbooster speed or shinespark pose
#TODO: how to handle super block in a tunnel? You don't necessarily need stand to destroy a super block...
#TODO: a "plant power bomb 'action' as part of the BFS? -> a fire super missile action...
# Gets very complex very quickly though...

any_velocity_type = set([VType.RUN, VType.SPEED, VType.WATER])
any_pose = set([SamusPose.STAND, SamusPose.MORPH, SamusPose.JUMP, SamusPose.SPIN])
any_interval = Interval(-Infinity, Infinity)
any_velocity = VelocitySet(any_interval, HVelocitySet(any_velocity_type, any_interval))

# The requirements to treat blocks as solid
# Cannot treat a tile as solid if either it is not in this data structure, or Samus does not meet one of
# the necessary requirements
block_solid_requirements = {
        AbstractTile.SOLID : [(any_velocity, ItemSet([]), any_pose)],
        AbstractTile.BLOCK_CRUMBLE: "Reciprocal",
        # Can treat a shot block as either solid or air depending on the situation
        AbstractTile.BLOCK_SHOT : [(any_velocity, ItemSet([]), any_pose)],
}

speed_hv = HVelocitySet(set([VType.SPEED]), Interval(30, Infinity))
# Exposing a weakness of velocity sets as single-sided intervals
speed_velocity_l = VelocitySet(any_interval, speed_hv)
speed_velocity_r = VelocitySet(any_interval, speed_hv.horizontal_flip())
# Negative vertical speed and no horizontal speed
crumble_velocity = VelocitySet(Interval(0, Infinity), HVelocitySet(any_velocity_type, Interval(0,0)))

# The requirements to treat blocks as air
block_air_requirements = {
    AbstractTile.AIR : [(any_velocity, ItemSet([]), any_pose)],
    AbstractTile.BLOCK_BOMB : [(any_velocity, ItemSet(["MB", "B"]), any_pose),
                               (any_velocity, ItemSet(["MB", "PB"]), any_pose),
                               (any_velocity, ItemSet(["SA"]), set([SamusPose.SPIN]))],
    AbstractTile.BLOCK_MISSILE : [(any_velocity, ItemSet(["M"]), any_pose)],
    AbstractTile.BLOCK_SUPER : [(any_velocity, ItemSet(["S"]), any_pose)],
    AbstractTile.BLOCK_POWER_BOMB : [(any_velocity, ItemSet(["MB", "PB"]), any_pose)],
    AbstractTile.BLOCK_GRAPPLE : [(any_velocity, ItemSet(["G"]), any_pose)],
    #TODO: speed can break bomb blocks
    AbstractTile.BLOCK_SPEED : [(speed_velocity_l, ItemSet(["SB"]), any_pose),
                                (speed_velocity_r, ItemSet(["SB"]), any_pose)],
    #TODO: can't actually treat /adjacent/ crumble blocks as air...
    # Positional requirements
    AbstractTile.BLOCK_CRUMBLE : [(crumble_velocity, ItemSet([]), any_pose)],
    AbstractTile.BLOCK_SHOT : [(any_velocity, ItemSet([]), any_pose)],
}

def meets(samus_state, requirement):
    v, i, p = requirement
    iv = samus_state.velocity in v
    ii = samus_state.items >= i
    ip = samus_state.pose in p
    return iv and ii and ip

def tile_is_air(samus_state, tile_type):
    if tile_type not in block_air_requirements:
        return False
    requirements = block_air_requirements[tile_type]
    for r in requirements:
        if meets(samus_state, r):
            return True
    return False

def tile_is_solid(samus_state, tile_type):
    if tile_type not in block_solid_requirements:
        return False #TODO: should this be return True? Look at the comment above block_solid_requirements
    requirements = block_solid_requirements[tile_type]
    if requirements == "Reciprocal":
        return not tile_is_air(samus_state, tile_type)
    for r in requirements:
        if meets(samus_state, r):
            return True
    return False

def tile_matches(samus_state, tile_type, as_what):
    assert as_what in [AbstractTile.SOLID, AbstractTile.AIR]
    if as_what == AbstractTile.SOLID:
        return tile_is_solid(samus_state, tile_type)
    elif as_what == AbstractTile.AIR:
        return tile_is_air(samus_state, tile_type)
    else:
        assert False, f"Invalid tile coercion: {as_what}"

def mk_pose_clause(poses):
    if poses == any_pose:
        return ""
    pose_eqs = []
    for p in poses:
        pose_eqs.append(f"pose_next = {p}")
    pose_clause = " | ".join(pose_eqs)
    pose_clause = f"({pose_clause})"
    return pose_clause

def mk_reqs_clause(requirements):
    rcs = []
    for vset, iset, poses in requirements:
        pose_clause = mk_pose_clause(poses)
        if vset == any_velocity:
            vel_clause = ""
        else:
            vel_clause = vset.as_tla(when="next")
        items_clause = itemset_as_tla(iset)
        rc = to_conj([pose_clause, vel_clause, items_clause])
        rcs.append(rc)
    return to_disj(rcs)

def tla_solid_coercion(tile_type):
    if tile_type not in block_solid_requirements:
        return None # True? (which would be "")
    requirements = block_solid_requirements[tile_type]
    if requirements == "Reciprocal":
        return "~" + tla_air_coercion(tile_type)
    else:
        return mk_reqs_clause(requirements)

def tla_air_coercion(tile_type):
    if tile_type not in block_air_requirements:
        return None
    requirements = block_air_requirements[tile_type]
    return mk_reqs_clause(requirements)

def tla_tile_coercion(tile_type, as_what):
    if as_what == AbstractTile.SOLID:
        return tla_solid_coercion(tile_type)
    elif as_what == AbstractTile.AIR:
        return tla_air_coercion(tile_type)
    else:
        assert False, f"Invalid tile coercion: {as_what}"

pose_hitboxes = {
    SamusPose.STAND: [Coord(0,0), Coord(0,1), Coord(0,2)],
    SamusPose.MORPH: [Coord(0,0)],
    SamusPose.JUMP: [Coord(0,0), Coord(0,1), Coord(0,2)],
    SamusPose.SPIN: [Coord(0,0), Coord(0,1)]
    }

def get_adj(pos, pose, direction):
    s_t = samus_tiles(pos, pose)
    h_t = [t + direction for t in s_t if t + direction not in s_t]
    return h_t

def get_horizontal_adj(s, scan_direction):
    h = Coord(scan_direction.x, 0)
    return get_adj(s, h)

def get_vertical_adj(s, scan_direction):
    v = Coord(0, scan_direction.y)
    return get_adj(s, v)

def get_all_adj(pos, pose):
    s_t = samus_tiles(pos, pose)
    l = []
    for t in s_t:
        ns = t.neighbors()
        for n in ns:
            if n not in s_t:
                l.append(n)
    return l

def check_collision(collide_tiles, new_origin, new_level, position):
    for b in collide_tiles:
        if new_level[b + position - new_origin] == AbstractTile.SOLID:
            return True
    return False

def samus_tiles(pos, pose):
    return [pos + t for t in pose_hitboxes[pose]]

def copy_items(items_list):
    return [(c.copy(), i.copy()) for c,i in self.items]

def parse_image_folder(folder):
    """ Helper function for LevelState.pretty_print() """
    image_dict = {}
    image_paths = Path(folder).glob("**/*.png")
    for image_path in image_paths:
        i = Image.open(str(image_path))
        image_dict[image_path.name] = i
    return image_dict

def samus_state_to_image(state, context_state):
    """ Helper function for LevelState.pretty_print() """
    # Use the context to determine the facing
    if context_state is not None:
        if state.position.x >= context_state.position.x:
            facing = "right"
        else:
            facing = "left"
    # Unknown -> arbitrary
    else:
        facing = "right"
        pass
    if state.pose == SamusPose.STAND:
        return "samus_stand_{}.png".format(facing)
    elif state.pose == SamusPose.MORPH:
        return "samus_morph.png"
    elif state.pose == SamusPose.JUMP:
        return "samus_jump_{}.png".format(facing)
    elif state.pose == SamusPose.SPIN:
        return "samus_spin_{}.png".format(facing)
    else:
        assert False, "Bad SamusPose"

# Helper for LevelState.pretty_print()
item_to_image = {
    ItemSet(["MB"]): "item_morph_outline.png",
    ItemSet(["M"]): "item_missile_outline.png",
    ItemSet(["B"]): "item_bombs_outline.png",
    ItemSet(["PB"]): "item_power_bomb_outline.png",
    ItemSet(["SPB"]): "item_springball_outline.png",
    ItemSet(["S"]): "item_super_outline.png",
    ItemSet(["G"]): "item_grapple_outline.png",
    ItemSet(["SA"]): "item_screw_outline.png",
    ItemSet(["V"]): "item_varia_outline.png",
    ItemSet(["GS"]): "item_gravity_outline.png",
    ItemSet(["SB"]): "item_speed_outline.png",
    ItemSet(["HJ"]): "item_hi_jump_outline.png",
    ItemSet(["CB"]): "item_charge_outline.png",
    ItemSet(["WB"]): "item_wave_outline.png",
    ItemSet(["WB"]): "item_wave_outline.png",
    ItemSet(["E"]): "item_energy_outline.png",
    ItemSet(["PLB"]): "item_plasma_outline.png",
    ItemSet(["Spazer"]): "item_spazer_outline.png",
    ItemSet(["RT"]): "item_reserve_outline.png",
    ItemSet(["XR"]): "item_xray_outline.png",
    ItemSet(["IB"]): "item_ice_outline.png",
    ItemSet(["SJ"]): "item_space_jump_outline.png",
    }

#TODO: WorldState object that includes doors / rooms
class LevelState(object):
    """
    Composable structure that keeps track of level data.
    """

    def __init__(self, origin, level, liquid_type, liquid_level, items, doors):
        self.origin = origin
        self.level = level
        # Set the writeable false flag in order to allow hashing
        self.level.flags.writeable = False
        self.liquid_type = liquid_type
        self.liquid_level = liquid_level
        #TODO: does items need to be copied?
        # Items is Coord -> ItemSet
        self.items = items
        # Doors is Coord -> (Room Name, Coord)
        self.doors = doors
        # Room Registry is Room Name -> LevelState

    #TODO: water?
    def to_image(self, tint_pos=set(), tint_color=None):
        i = Image.new("RGB", (self.shape[0], self.shape[1]))
        pixels = i.load()
        # Set the pixels
        for xy in self.rect:
            pixel_xy = xy - self.origin
            if xy in tint_pos:
                pixels[pixel_xy] = tint_color
            if xy in self.items:
                pixels[pixel_xy] = item_color
            else:
                pixels[pixel_xy] = abstract_to_color[self[xy]]
        return i

    def pretty_print(self, samus_states, image_folder):
        tile_size = 16
        scale_factor = Coord(tile_size, tile_size)
        center = Coord(tile_size // 2, tile_size // 2)
        image_dict = parse_image_folder(image_folder)
        i = Image.new("RGB", (self.shape[0] * tile_size, self.shape[1] * tile_size))
        pixels = i.load()
        # Draw the level
        for xy in self.rect:
            pos = (xy - self.origin) * scale_factor
            # Have to convert pos from coord to tuple for some reason
            i.paste(image_dict[abstract_to_image[self[xy]]], (pos[0], pos[1]))
        # Draw the samusstates
        # Draw s0 arbitrarily first
        if len(samus_states) > 0:
            s0_i = image_dict[samus_state_to_image(samus_states[0], None)]
            pos = (samus_states[0].position - self.origin) * scale_factor
            i.paste(s0_i, (pos[0], pos[1]))
        # Samus facing direction depends on context of previous state
        for s1, s2 in pairwise(samus_states):
            s_i = image_dict[samus_state_to_image(s2, s1)]
            pos = (s2.position - self.origin) * scale_factor
            i.paste(s_i, (pos[0], pos[1]))
        # Draw the items (over the samuses, to make them visible, under the arrows)
        for pos, item in self.items.items():
            if item in item_to_image:
                item_image = image_dict[item_to_image[item]]
            else:
                # Handles bosses, etc.
                item_image = image_dict["item_unknown.png"]
            real_pos = pos * scale_factor
            i.paste(item_image, (real_pos[0], real_pos[1]))
        # Convert to numpy to draw arrows using CV2
        numpy_i = np.array(i)
        # Draw the arrows connecting the samusstates
        for index, (s1, s2) in enumerate(pairwise(samus_states)):
            p1 = s1.position * scale_factor + center
            p2 = s2.position * scale_factor + center
            # cv2 does not have fixed size tiplengths
            length = p2.euclidean(p1)
            if length == 0:
                tiplength = 0
            else:
                tiplength = 10 / length
            percent = index / len(samus_states)
            c_index = int(255 * percent)
            color = (c_index, 0, 255 - c_index)
            cv2.arrowedLine(numpy_i, p1, p2, color, 2, tipLength=tiplength)
        # Convert back to PIL
        i = Image.fromarray(numpy_i)
        return i

    @property
    def shape(self):
        return Coord(self.level.shape[0], self.level.shape[1])

    @property
    def rect(self):
        return Rect(self.origin, self.origin + self.shape)

    def paste(self, level_origin, level):
        assert self.origin + self.shape <= Coord(level.shape[0], level.shape[1])
        o = self.origin - level_origin
        assert o >= Coord(0,0), o
        level[o.x:o.x + self.shape.x, o.y:o.y + self.shape.y] = self.level

    def copy(self):
        l = np.copy(self.level)
        return LevelState(self.origin.copy(), l, self.liquid_type, self.liquid_level, self.items)

    def __hash__(self):
        return hash((self.origin, bytes(self.level.data), self.liquid_type, self.liquid_level))

    def __eq__(self, other):
        o = self.origin == other.origin
        l = np.array_equal(self.level, other.level)
        s = self.shape == other.shape
        lt = self.liquid_type == other.liquid_type
        ll = self.liquid_level == other.liquid_level
        return o and l and s and lt and ll

    def __getitem__(self, index):
        internal_index = index - self.origin
        # Do not allow negative indexing
        if Coord(0,0) > internal_index:
            raise IndexError(internal_index)
        return self.level[internal_index]

    def in_bounds(self, index):
        try:
            t = self[index]
            return True
        except IndexError:
            return False

    def horizontal_flip(self):
        o = self.origin.copy()
        d = np.copy(self.level)
        d = np.flip(d, 0)
        #TODO: self.items must be flipped
        assert False
        return LevelState(o, d, self.liquid_type, self.liquid_level, self.items)

def tla_collide(conflict_ds, inter_pos, inter_pose, original_vs):
    for d, clause in conflict_ds.items():
        # Landing on the ground
        vv, vh, vt = original_vs
        if d == Coord(0, 1):
            vt = f"vt_next = {VType.RUN}"
            vh = f"vh_next = 0"
            vv = f"vv_next = 0"
            vc = to_conj([vt, vh, vv, clause])
            # Any non-morph pose leaves you standing
            pos = inter_pos
            if inter_pose == SamusPose.SPIN:
                pose = SamusPose.STAND
                pos = inter_pos + Coord(0, -1)
            elif inter_pose != SamusPose.MORPH:
                pose = SamusPose.STAND
            else:
                pose = SamusPose.MORPH
            yield (vc, pos, pose)
        # Colliding with the ceiling
        elif d == Coord(0, -1):
            vv = f"vv_next = 0"
            vc = to_conj([vt, vh, vv, clause])
            yield (vc, inter_pos, inter_pose)
        # Colliding with a wall (kill horizontal velocity)
        else:
            vt = f"vt_next = {VType.RUN}"
            vh = f"vh_next = 0"
            vc = to_conj([vt, vh, vv, clause])
            yield (vc, inter_pos, inter_pose)

class SamusState(object):

    def __init__(self, position, velocity, items, pose):
        self.position = position
        self.velocity = velocity
        self.items = items
        self.pose = pose

    def __geq__(self, other):
        p = self.position == other.position
        v = self.velocity == other.velocity
        #TODO: what about negative speed?
        i = self.items >= other.items
        pose = self.pose == other.pose
        return p and v and i and pose

    def meets(self, other):
        v = self.velocity >= other.velocity
        i = self.items >= other.items
        pose = self.pose == other.pose
        return v and i and pose

    def copy(self):
        return SamusState(self.position.copy(),
                self.velocity.copy(), self.items.copy(), self.pose)

    def __hash__(self):
        return hash((self.position, self.velocity, self.items, self.pose))

    def __eq__(self, other):
        p = self.position == other.position
        v = self.velocity == other.velocity
        i = self.items == other.items
        pose = self.pose == other.pose
        return p and v and i and pose

    def __repr__(self):
        return "({}|{}|{}|{})".format(self.position, self.velocity, self.pose.name, self.items)

    def horizontal_flip(self, level):
        position = self.position.flip_in_rect(level.rect, Coord(1, 0))
        v = self.velocity.horizontal_flip()
        i = self.items.copy()
        p = self.pose
        return SamusState(position, v, i, p)

    def collide(self, ds, debug=False):
        s = self.copy()
        v = self.velocity
        for d in ds:
            # Landing on the ground
            if d == Coord(0, 1):
                vh = HVelocity(VType.RUN, 0)
                vv = 0
                v = Velocity(vv, vh)
                s.velocity = v
                # Any non-morph pose leaves you standing
                #TODO: what if you're in spin in a 2-high gap -- this will clip you into the floor!!
                if s.pose == SamusPose.SPIN:
                    s.pose = SamusPose.STAND
                    s.position += Coord(0, -1)
                elif s.pose != SamusPose.MORPH:
                    s.pose = SamusPose.STAND
            # Colliding with the ceiling
            elif d == Coord(0, -1):
                vv = 0
                v = Velocity(vv, s.velocity.vh.copy())
                s.velocity = v
            # Colliding with a wall (kill horizontal velocity)
            else:
                vh = HVelocity(VType.RUN, 0)
                v = Velocity(s.velocity.vv, vh)
                s.velocity = v
        if debug:
            print("Collided")
            print(s)
        return s

class SearchState(object):

    def __init__(self, samus, level):
        self.samus = samus
        self.level = level

    #TODO: water?
    def to_image(self, tint_pos=set(), tint_color=None):
        return self.level.to_image(tint_pos, tint_color)

    def __hash__(self):
        return hash((self.samus, self.level))

    def __eq__(self, other):
        return self.samus == other.samus and self.level == other.level

    def copy(self):
        return SearchState(self.samus.copy(), self.level.copy())

    def horizontal_flip(self):
        samus = self.samus.horizontal_flip(self.level)
        level = self.level.horizontal_flip()
        return SearchState(samus, level)

SFClauses = namedtuple("SFClauses", ["pos_prev", "pos_next", "pose_prev", "pose_next", "item", "vel"])

def mk_pos_clause(pos, when="prev"):
    assert when == "prev" or when == "next"
    x = f"x_{when} = {pos.x}"
    y = f"y_{when} = {pos.y}"
    return to_conj([x,y])

class SamusFunction(object):
    def __init__(self, vfunction, required_items, gain_items, before_pose, after_pose, after_position):
        # before_position is always (0,0), after_position is relative
        self.vfunction = vfunction
        self.required_items = required_items
        self.gain_items = gain_items
        self.before_pose = before_pose
        self.after_pose = after_pose
        self.after_position = after_position

    def apply(self, samus_state, item_locations):
        # First, check if samus has the items to complete the rule
        if not samus_state.items >= self.required_items:
            return None, "Missing items"
        # Then, check that items that are picked up during the rule actually exist:
        items = samus_state.items.copy()
        for g in self.gain_items:
            rel_position = g + samus_state.position
            if rel_position not in item_locations:
                return None, "Required item does not exist"
            # Add the item picked up from that location
            items |= item_locations[rel_position]
        # Samus automatically gains items if her hitbox is inside an item.
        # Important for model checking because skipping items can be "beneficial" from that
        # point of view, so we want to model more precisely when an item can be skipped.
        #TODO: Need to also check this during application of a LevelFunction that moves through an item
        # using collide()
        t = samus_tiles(samus_state.position, samus_state.pose)
        for tile in t:
            if tile in item_locations:
                # It's okay to allow samus to pick up the same item multiple times
                items |= item_locations[tile]
        # Determine the new velocity
        v = self.vfunction.apply(samus_state.velocity)
        if v is None:
            return None, "Velocity application failed"
        if not samus_state.pose == self.before_pose:
            return None, "Incorrect pose, in {} but needed {}".format(samus_state.pose, self.before_pose)
        else:
            pose = self.after_pose
        position = samus_state.position + self.after_position
        return SamusState(position, v, items, pose), None

    def as_tla(self, position, item_locations):
        # POSITION
        #TODO: don't need to include x_prev
        pos_prev = mk_pos_clause(position)
        after_pos = position + self.after_position
        pos_next = mk_pos_clause(after_pos, "next")

        # ITEMS
        # items_unchanged macro using let in
        item_clauses = []
        item_clauses.append(itemset_as_tla(self.required_items))
        gained_items = ItemSet()
        for g in self.gain_items:
            rel_position = g + position
            if rel_position not in item_locations:
                return None, "Required item does not exist"
            # Add the item picked up from that location
            gained_items |= item_locations[rel_position]
        for g in gained_items:
            clause = f"{g}_next = 1"
            item_clauses.append(clause)
        # For any item that is not gained, it must remain constant
        #TODO: Items unchanged operator
        for i in item_mapping:
            if not (i in gained_items):
                clause = f"{i}_prev = {i}_next"
                item_clauses.append(clause)
        item_clause = to_conj(item_clauses)
        # VELOCITY
        vclause = self.vfunction.as_tla()
        #vclause = f"({vclause})"
        # POSE
        pose_prev = f"pose_prev = {self.before_pose}"
        pose_next = f"pose_next = {self.after_pose}"
        return SFClauses(pos_prev, pos_next, pose_prev, pose_next, item_clause, vclause)

    def horizontal_flip(self):
        vnew = self.vfunction.horizontal_flip()
        # Flip the after position horizontally too
        pnew = Coord(-self.after_position.x, self.after_position.y)
        # Flip the set of items obtained
        new_items = [Coord(-g.x, g.y) for g in self.gain_items]
        return SamusFunction(vnew, self.required_items, new_items, self.before_pose, self.after_pose, pnew)

    def compose(self, other):
        vnew = self.vfunction.compose(other.vfunction)
        if vnew is None:
            return None
        # Need the items for both
        r_i = self.required_items | other.required_items
        # Gain the items from both
        # Compute the new positions of the other items based on where other is applied relative to self
        #TODO: this is the "strict" version, where items that you pick up during a long rule
        # cannot be used later in the rule.
        # It is also possible to have a "loose" version, where composition picks an instantiation of the item
        # Then allows use of that item later, but then stores a constraint list for which instantiation was chosen
        # During application, both the positions of items and the explicit instantiation (if any) would be checked.
        # This roughly corresponds to the composition semantics for having a different rule for each item that would
        # be picked up.
        other_items_rel = [o + self.after_position for o in other.gain_items]
        g_i = self.gain_items + other_items_rel
        # Incompatible if the poses don't align
        if not self.after_pose == other.before_pose:
            return None
        else:
            b_p = self.before_pose
            a_p = other.after_pose
        pnew = self.after_position + other.after_position
        return SamusFunction(vnew, r_i, g_i, b_p, a_p, pnew)

class IntermediateState(object):
    def __init__(self, pos, walls, airs, function):
        self.pos = pos
        self.walls = walls
        self.airs = airs
        self.samusfunction = function

    def horizontal_flip(self):
        pos = Coord(-self.pos.x, self.pos.y)
        walls = [Coord(-w.x, w.y) for w in self.walls]
        airs = [(Coord(-d.x, d.y), [Coord(-t.x, t.y) for t in tiles]) for d, tiles in self.airs]
        if self.samusfunction is None:
            sf = None
        else:
            sf = self.samusfunction.horizontal_flip()
        return IntermediateState(pos, walls, airs, sf)

    def shift(self, pos):
        s = self.samus_state.copy()
        s += pos
        w = self.walls.copy()
        a = self.airs.copy()
        return IntermediateState(s,w,a,self.samusfunction)

def level_check_and_make(level, origin, tile, tile_type, samus_state):
    """Helper for LevelFunction apply()"""
    l = get_tile(level, origin, tile)
    if l is None:
        return None
    # Don't need to overwrite if there's already a tile that matches
    if tile_matches(samus_state, l, tile_type):
        return "ok"
    if l == AbstractTile.UNKNOWN:
        level[tile] = tile_type
        return "ok"
    else:
        return None

def tla_tile_clause(level, origin, tile, tile_type):
    l = get_tile(level, origin, tile)
    if l is None:
        return None
    if l == AbstractTile.UNKNOWN:
        #TODO: probably don't want TLA check to do be able to edit the level...
        assert False, f"Abstract tile in tla mode: {tile}"
        level[tile] = tile_type
        return ""
    else:
        return tla_tile_coercion(l, tile_type)

def level_check(level, origin, tile, tile_type, samus_state):
    l = get_tile(level, origin, tile)
    if l is None:
        return None
    if tile_matches(samus_state, l, tile_type):
        return True
    else:
        return False

def get_tile(level, origin, tile):
    index = tile - origin
    try:
        # Do not allow negative indexing
        if Coord(0,0) > index:
            return None
        return level[index]
    # Do not allow oob indexing
    except IndexError:
        return None

def samus_occupies_air_tla(pos, pose, origin, level_array):
    clauses = []
    for t in samus_tiles(pos, pose):
        ok = tla_tile_clause(level_array, origin, t, AbstractTile.AIR)
        # Could not coerce the tiles samus occupies to air
        if ok is None:
            return None
        clauses.append(ok)
    return to_conj(clauses)

class LevelFunction(object):

    def __init__(self, liquid_type, liquid_interval, state_list):
        self.liquid_type = liquid_type
        self.liquid_interval = liquid_interval
        self.state_list = state_list

    #TODO: version that does not copy the levelstate for a level that is not modified
    def apply(self, state, debug=False):
        # Convenience variables
        sl = state.level
        origin = sl.origin
        # Check liquid interval compatibility
        # Get absolute interval from relative
        l_i = self.liquid_interval.shift(state.samus.position.y)
        # Cannot apply if liquid does not match
        if self.liquid_type != LiquidType.NONE and state.level.liquid_level not in l_i:
            return None, "Liquid level does not match!"
        # If the liquidtype is none, that means that ANY liquidtype is acceptable
        # within the designated interval
        if self.liquid_type != LiquidType.NONE and state.level.liquid_type != self.liquid_type:
            print(state.level.liquid_type)
            print(self.liquid_type)
            return None, "Liquid type does not match!"
        # Copy level state for editing
        level_array = np.copy(state.level.level)
        level_array.flags.writeable = True
        # Now envision each intermediate state sequentially
        intermediate_samus = state.samus.copy()
        for i_state in self.state_list:
            # Get the intermediate absolute samus state from relative
            # If it's a "key" state, use the built-in function to determine pose / velocity, etc.
            if i_state.samusfunction is not None:
                intermediate_samus, _ = i_state.samusfunction.apply(intermediate_samus, sl.items)
                assert intermediate_samus is not None
            intermediate_samus.position = i_state.pos + state.samus.position
            #print("intermediate samus:{}".format(intermediate_samus))
            #print("walls", i_state.walls)
            #print("airs:", i_state.airs)
            # Check and create necessary airs
            for t in samus_tiles(intermediate_samus.position, intermediate_samus.pose):
                ok = level_check_and_make(level_array, origin, t, AbstractTile.AIR, intermediate_samus)
                # Could not reconcile air to the tiles samus occupies
                if ok is None:
                    return None, "Samus occupies solid tiles at {}".format(t)
            # Check and create necessary solids
            for rel_t in i_state.walls:
                g_t = rel_t + intermediate_samus.position
                ok = level_check_and_make(level_array, origin, g_t, AbstractTile.SOLID, intermediate_samus)
                # Could not reconcile solid to the tiles indicated as walls
                if ok is None:
                    return None, "Required wall not present at {}".format(g_t)
            # Check nearby airs for partial application
            conflict_ds = []
            for d, rel_ts in i_state.airs:
                conflict = False
                # Collect partial application conflicts
                for rel_t in rel_ts:
                    g_t = rel_t + intermediate_samus.position
                    # There's a conflict if a required tile cannot be reconciled to air
                    if not level_check_and_make(level_array, origin, g_t, AbstractTile.AIR, intermediate_samus):
                        conflict = True
                if conflict:
                    conflict_ds.append(d)
            # If there was a conflict, partially apply the rule
            if len(conflict_ds) > 0:
                #TODO: Crouch pose for allowing 2-high passageways to function
                samus = intermediate_samus.collide(conflict_ds, debug)
                i_samus_tiles = samus_tiles(samus.position, samus.pose)
                # Try to assign air to all the tiles that samus winds up in after the collision
                for t in i_samus_tiles:
                    ok = level_check_and_make(level_array, origin, t, AbstractTile.AIR, samus)
                    if ok is None:
                        #print("Collide Fail")
                        #print(t, level_array[t])
                        #print(state.samus)
                        #print(samus)
                        #print(i_samus_tiles)
                        return None, "Collision clipped Samus into a wall at {}".format(t)
                level = LevelState(sl.origin.copy(), level_array, sl.liquid_type, sl.liquid_level, sl.items, sl.doors)
                return SearchState(samus, level), None
        # No conflict
        # Create the new level state
        level = LevelState(sl.origin.copy(), level_array, sl.liquid_type, sl.liquid_level, sl.items, sl.doors)
        return SearchState(intermediate_samus, level), None

    #TODO: version that does not copy the levelstate for a level that is not modified
    def as_tla(self, pos, level, debug=False):
        # Convenience variables
        origin = level.origin
        # Check liquid interval compatibility
        # Get absolute interval from relative
        #TODO: How to create a "branch" where Samus has gravity if we are applying a rule where grav is required?
        l_i = self.liquid_interval.shift(pos.y)
        # Cannot apply if liquid does not match
        if self.liquid_type != LiquidType.NONE and level.liquid_level not in l_i:
            return None
        # If the liquidtype is none, that means that ANY liquidtype is acceptable
        # within the designated interval
        if self.liquid_type != LiquidType.NONE and level.liquid_type != self.liquid_type:
            print(level.liquid_type)
            print(self.liquid_type)
            return None
        # Copy level state for editing
        #TODO: Version without copying that cannot edit
        level_array = np.copy(level.level)
        level_array.flags.writeable = True
        # Now envision each intermediate state sequentially
        key_states = [s for s in self.state_list if s.samusfunction is not None]
        if len(key_states) > 1:
            assert False, "Cannot convert compound rule to TLA"
        assert self.state_list[0].samusfunction is not None, "No initial keystate!"
        # Get state requirements based on the samsufunction (without regard to level design)
        sf = self.state_list[0].samusfunction
        inter_pose = sf.after_pose
        sf_clauses = sf.as_tla(pos, level.items)
        # Can happen if you try to invoke an item pickup rule at the wrong location
        if sf_clauses[0] is None:
            return None
        # Unpack clauses from the statefunction
        pos_prev_c, pos_next_c, pose_prev_c, pose_next_c, item_c, v_clauses = sf_clauses
        domain_c, vv_c, vh_c, vt_c = v_clauses
        # Nexts and v_nexts may change based on the outcome of applying the rule
        preconditions = to_conj([pos_prev_c, pose_prev_c, domain_c, item_c])
        path_conditions = []
        inter_invalid = False
        possible_outcomes = []
        for n,i_state in enumerate(self.state_list):
            # Get the intermediate absolute samus state from relative
            inter_pos = i_state.pos + pos
            pos_clause = mk_pos_clause(inter_pos, "next")
            if debug:
                print(f"Iteration {n}")
                print(inter_pos)
                print(pos_clause)
                print(f"Walls: {i_state.walls}")
                print(f"Airs: {i_state.airs}")
            in_air = samus_occupies_air_tla(inter_pos, inter_pose, origin, level_array)
            # Could not coerce the tiles indicated as airs to air
            # Samus is definitely (not just possibly) going to collide here
            if in_air is None:
                inter_invalid = True
            walls_present = []
            # Check and create necessary solids
            for rel_t in i_state.walls:
                g_t = rel_t + inter_pos
                ok = tla_tile_clause(level_array, origin, g_t, AbstractTile.SOLID)
                # Could not coerce the tiles indicated as walls to solid
                if ok is None:
                    inter_invalid = True
                    break
                walls_present.append(ok)
            walls_present = to_conj(walls_present)
            path_conditions.append(in_air)
            path_conditions.append(walls_present)
            if debug:
                print(path_conditions)
            if inter_invalid:
                break
            # Check nearby airs for partial application
            # For each conflict, the conditions under which samus would collide)
            conflict_ds = {}
            for d, rel_ts in i_state.airs:
                d_conflict = []
                # Collect partial application conflicts
                for rel_t in rel_ts:
                    g_t = rel_t + inter_pos
                    # There's a conflict if a required tile cannot be reconciled to air
                    is_air = tla_tile_clause(level_array, origin, g_t, AbstractTile.AIR)
                    # A tile that's impossible to coerce to air will always cause a collision
                    if is_air is None:
                        conflict_clause = ""
                    # A tile that's always possible to coerce to air will never cause a collision
                    elif len(is_air) == 0:
                        continue
                    # Other tiles that may or may not be coerced to air cause a conflict when they can't be
                    else:
                        conflict_clause = f"~({is_air})"
                    d_conflict.append(conflict_clause)
                # Any of the requirements failing causes a conflict
                if len(d_conflict) > 0:
                    conflict_ds[d] = to_disj(d_conflict)
            # Either there is a conflict in one of the directions, or no conflicts
            if debug:
                print(conflict_ds)
            # Use path_conditions to produce an intermediate state
            # Append to valid_inters
            # All possible collision outcomes, along with their constraints
            collide_outcomes = tla_collide(conflict_ds, inter_pos, inter_pose, (vv_c, vh_c, vt_c))
            # Collision detection on the outgoing states
            for clause, collide_pos, pose in collide_outcomes:
                in_air = samus_occupies_air_tla(collide_pos, pose, origin, level_array)
                # Impossible collision - Samus would occupy something
                if in_air is None:
                    continue
                pos_clause = mk_pos_clause(collide_pos, when="next")
                pose_clause = f"pose_next = {pose}"
                outcome = to_conj([pos_clause, pose_clause, in_air, clause, to_conj(path_conditions)])
                possible_outcomes.append(outcome)
            # For Samus to advance to the next state, she must not have collided
            # This prevents moving through multiple blocks of the same type from creating multiple outcomes
            for clause in conflict_ds.values():
                if len(clause) > 0:
                    path_conditions.append(f"~({clause})")
                # If the conflict_d condition is empty, then the collision always happens and the rest of the rule cannot be applied.
                else:
                    inter_invalid = True
            if inter_invalid:
                break
        #TODO: Think about this further -- the loop may terminate early
        # End State (rule fully applied successfully, or aborted mid-rule)
        if not inter_invalid:
            pos_clause = mk_pos_clause(inter_pos, when="next")
            assert pos_clause == pos_next_c, f"Final positions do not match {pos_clause} -- {pos_next_c}"
            pose_clause = f"pose_next = {inter_pose}"
            assert pose_clause == pose_next_c, f"Final poses do not match {pose_clause} -- {pose_next_c}"
            v_clause = to_conj([vv_c, vh_c, vt_c])
            end_state = to_conj([pos_clause, pose_clause, v_clause, to_conj(path_conditions)])
            possible_outcomes.append(end_state)
        if debug:
            print("Outcomes:")
            for i,o in enumerate(possible_outcomes):
                print(f"Outcome {i}: {o}")
        if len(possible_outcomes) > 0:
            all_end_states = to_disj(possible_outcomes)
            return f"{preconditions} & {all_end_states}"
        else:
            return None

    def horizontal_flip(self):
        l = self.liquid_type
        i = self.liquid_interval.copy()
        sl = [state.horizontal_flip() for state in self.state_list]
        return LevelFunction(l, i, sl)

    def get_end_pos(self):
        state, _ = self.state_list[-1]
        return state.position

    def compose(self, other):
        # Check liquid type compatibility
        if self.liquid_type != other.liquid_type:
            return None
        new_lt = self.liquid_type
        # Other liquid level interval is relative to the ending position
        end_p = self.get_end_pos()
        rel_other_interval = other.liquid_interval.shift(end_p.y)
        new_li = self.liquid_interval.intersect(rel_other_interval)
        # Incompatible if they require disjoint liquid placements
        if new_li is None:
            return None
        # Check compatibility of states
        self_end = self.state_list[-1][0]
        other_start = other.state_list[0][0]
        #TODO:
        if not self_end >= other_start:
            return None
        # Update relative positions for the other rule
        #TODO: or store states relative to each other
        other_relstates = [state.shift(end_p) for state in other.state_list]
        new_state_list = self.state_list + other_relstates
        return LevelFunction(new_lt, new_li, new_state_list)

class StateFunction(object):

    #TODO: Fix the names please!
    def __init__(self, name, state_function, level_transition, cost):
        self.name = name
        self.state_function = state_function
        self.level_transition = level_transition
        self.cost = cost

    def apply(self, state, debug=False):
        if debug:
            print("Applying Rule: {}".format(self.name))
            print(state.samus)
        new_s, s_err = self.state_function.apply(state.samus, state.level.items)
        # Cannot be applied
        if new_s is None:
            return None, s_err
        # Applying the level transition accesses the entire state
        new_l, l_err = self.level_transition.apply(state, debug)
        # Cannot be applied
        if new_l is None:
            return None, l_err
        # If it was applied, get the level information and the samus state from the
        # level information and the (possibly partially applied) level transition
        if debug:
            print("New:")
            print(new_l.samus)
        return new_l, None

    def horizontal_flip(self):
        #TODO: do names even make sense? hf of hf is the same transition with a different name, for example...
        new_name = self.name + "_hf"
        new_sf = self.state_function.horizontal_flip()
        new_lt = self.level_transition.horizontal_flip()
        return StateFunction(new_name, new_sf, new_lt, self.cost)

    def compose(self, other):
        new_name = self.name + other.name
        new_sf = self.state_function.compose(other.state_function)
        new_lt = self.level_transition.compose(other.level_transition)
        #TODO Do the costs add? How to represent discounts?
        new_cost = self.cost + other.cost
        if new_sf is None or new_lt is None:
            return None
        else:
            return StateFunction(self, new_name, new_sf, new_lt, new_cost)

class Transition(object):

    def __init__(self, requirement, end_state):
        self.requirement = requirement
        self.end_state = end_state

    def horizontal_flip(self):
        r = self.requirement.horizontal_flip(self.end_state.level)
        e = self.end_state.horizontal_flip()
        return Transition(r, e)

class Rule(object):

    def __init__(self, name, transition_list, base_cost, extra_costs = None):
        #TODO: assert that each final state meets the requirements of the next state
        self.name = name
        self.transitions = transition_list
        self.base_cost = base_cost
        if extra_costs is None:
            self.extra_costs = []
        else:
            self.extra_costs = extra_costs

    def horizontal_flip(self):
        name = self.name + "_h"
        flipped_transitions = [t.horizontal_flip() for t in self.transitions]
        #TODO: how to structure the cost?
        base_cost = self.base_cost
        extra_costs = self.extra_costs.copy()
        return Rule(name, flipped_transitions, base_cost, extra_costs)

    @property
    def cost(self):
        return self.base_cost + sum(self.extra_costs)

    #TODO
    # Composes two Rules
    def __add__(self, other):
        before_state = self.before_state
        #TODO: recompute offset
        #TODO: check compatibility of self.after with other.before
        #TODO: compute levels
        #TODO: discounting scheme
        base_cost = self.base_cost + other.base_cost
        extra_costs = self.extra_costs + other.extra_costs
        return Rule(before_state, after_state, base_cost, extra_costs)


def uni(my_str):
	dictionary = {}
	words = my_str.split(',')

	for word in words:
		if word in dictionary:
			dictionary[word]=+1
		else:
			dictionary[word] = 1

	for i in dictionary:
		if dictionary[i] >1:

		    print(dictionary.pop(word))

	return sorted(dictionary.keys())

print(uni('red,white,black,red,green,black'))

#items = input("Input comma separated sequence of words")
#words = [word for word in items.split(",")]
#print(",".join(sorted(list(set(words)))))


#!/usr/bin/env python
# -*- coding: utf-8 -*-

from mastodon import Mastodon
from config import *

def tootea(toot):
    mastodon = Mastodon(
        access_token = M_ACCESS_TOKEN,
        api_base_url = M_API_BASE_URL
    )
    mastodon.status_post(toot)

if __name__ == '__main__':
    tootea("Proba")


import unittest
from scrapers import logger
import logging
from scrapers.statcan.statcan_dairy_trade import StatcanDairy
import os

class TestStatcanDairy(unittest.TestCase):

    test = StatcanDairy(CSV_FOLDER_PATH=os.environ.get("CSV_UNIT_TEST_PATH"))

    def test_statcan_dairy_scraper(self):
        statcan_logger = logger.get_logger("Statcan_logger")
        try:
            self.test.scrape()
            logging.info("Statcan returned filled CSV file")

        except Exception as e:
            statcan_logger.error("Error: crashed at testcode", str(e))
        statcan_logger.info('finished testing')
if __name__ == "__main__":
    unittest.main()


from collections import deque

def solution(queue1, queue2):
    nq1 = deque(queue1)
    nq2 = deque(queue2)
    s1 = sum(nq1)
    s2 = sum(nq2)
    cnt = 0

    # len*2 +2 인 이유
    # 한쪽에 값이 쏠려있는 형태가 대표적. 예시) [1,1,1,8,10,9] [1,1,1,1,1,1] (14회 확인필요)
    for i in range(len(queue1)*2 + 2):
        if s1 == s2:
            break

        if s1 > s2:
            temp = nq1.popleft()
            nq2.append(temp)
            s1 -= temp
            s2 += temp
            cnt += 1
        else:
            temp = nq2.popleft()
            nq1.append(temp)
            s1 += temp
            s2 -= temp
            cnt += 1

    if sum(nq1) != sum(nq2):
        cnt = -1

    return cnt

Programmers/2022 KAKAO TECH INTERNSHIP/118667.py


"""Farragone Qt UI job configuration.

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."""

import string
import re
import os
import locale
from html import escape

from ... import util, conf, core
from ...core.field import Alignments, SortTypes
from . import doc, qt, widgets

def optimise_paths (cwd, paths):
    """Make paths relative if it makes them simpler.

cwd: directory relative paths are relative to
paths: iterable of paths

"""
    for path in paths:
        rel = os.path.relpath(path, cwd)
        # we call a path simpler if it has at least 2 fewer path components
        # this is just a rough count, no need to be accurate (would involve
        # os.path.split in a loop)
        yield rel if path.count(os.sep) - rel.count(os.sep) >= 2 else path

class Dynamic:
    """Has a 'new_widget' signal for when a widget is added.

For widgets added outside the initialisation call, anywhere within this widget
or layout.

"""
    new_widget = qt.pyqtSignal()

class Changing:
    """Has a 'changed' signal for when user input changes."""
    changed = qt.pyqtSignal()

class FieldContext (qt.QComboBox):
    """A combo box widget providing a choice of field `Context`."""

    def __init__ (self):
        qt.QComboBox.__init__(self)
        self.setToolTip(_('The part of the path to work with'))

        widgets.add_combobox_items(self, *(
            {
                qt.Qt.UserRole: context,
                qt.Qt.DisplayRole: context.name,
                qt.Qt.ToolTipRole: context.desc
            } for context in core.field.all_contexts
        ))

    @property
    def context (self):
        """Get the currently selected `Context`."""
        return self.currentData()

class OrderingPadding (Dynamic, qt.QGridLayout):
    """Layout containing widgets for 'ordering' field padding settings.

files: `FilesSection`
changed: function to call when any settings change

"""

    # icon names by `Alignment` instance
    ALIGNMENT_ICONS = {
        Alignments.right: 'format-justify-right',
        Alignments.centre: 'format-justify-center',
        Alignments.left: 'format-justify-left'
    }

    def __init__ (self, files, changed):
        Dynamic.__init__(self)
        qt.QGridLayout.__init__(self)
        self._files = files
        self._options = []
        self._enabled = enabled = qt.QCheckBox(_('Padding'))
        self.addWidget(enabled, 0, 0, 1, 3)
        enabled.setToolTip(_('Add padding around the number within the field'))
        enabled.stateChanged.connect(lambda: self._toggle(changed))

        self._char = char = qt.QLineEdit()
        self._options.append(char)
        widgets.set_text_desc(char, _('Padding character'))
        char.setMaxLength(1)
        char.setText('0')
        char.textChanged.connect(changed)

        self._align = align = qt.QComboBox()
        self._options.append(align)
        align.setToolTip(_('Alignment of the number within the field value'))
        widgets.add_combobox_items(align, *(
            {
                'icon': self.ALIGNMENT_ICONS[alignment],
                qt.Qt.UserRole: alignment,
                qt.Qt.DisplayRole: alignment.name
            } for alignment in core.field.all_alignments
        ))
        align.currentIndexChanged.connect(changed)

        self._size = size = qt.QSpinBox()
        self._options.append(size)
        size.setRange(0, 99)
        size.setValue(0)
        size.setToolTip(_('Minimum padded size'))
        size.valueChanged.connect(changed)
        # maps to min value (0, which is also the default)
        size.setSpecialValueText(_('Auto'))

        for i, w in enumerate(self._options):
            self.addWidget(w, 1, i)
        self._toggle(lambda: None)

    def _is_enabled (self):
        return self._enabled.isChecked()

    # called when padding is enabled or disabled
    def _toggle (self, changed):
        enabled = self._is_enabled()
        for w in self._options:
            w.setVisible(enabled)
        changed()
        self.new_widget.emit()

    def get_fmt (self, interrupt=None):
        """Get a formatting function for the current settings.

interrupt: abort when this function returns True

For use as the `fmt` argument to `Ordering`.

"""
        if self._is_enabled():
            size = self._size.value()
            if size == 0: # auto
                size = core.field.Ordering.auto_padding_size(
                    self._files.inputs, interrupt)
            return core.field.Ordering.padding_fmt(
                self._char.text() or ' ', self._align.currentData(), size)
        else:
            return str

class CustomList (Dynamic, Changing, qt.QVBoxLayout):
    """User-modifiable list of items, arranged vertically.

types: sequence of definitions of types of items, where the order is preserved
       when presented, with the first the default; each is a dict with keys:
    id: unique identifier
    name: displayed name
    description: short description
    create: function called like
        `create(changed) -> {'data': data, 'item': item[, 'focus': focus] }` to
        create an item of this type
            changed: function to call with no arguments when any of the form
                     fields in the item change
            data: passed to `get_state`
            item: the created item (QWidget or QLayout)
            focus: widget to focus when added (default: `item`)
    getstate: optional function called with `data` returned by `create`, and a
              function `interrupt`; returns a representation of the item's
              current state
add_tooltip: tooltip for the button to add an item
rm_tooltip: tooltip for the button to remove an item

Emits `new_widget` signal on additions.

Attributes:

types: {type['id']: type} from `types` argument
items: sequence of current items, each a dict with keys:
    type: type['id']
    data: from `create`
    item: from `create`
    changed: called whenever the item's settings change to update `state`
    getstate: item_type's `getstate`
    state: from `getstate`, None if no `getstate` (possibly missing, use
           `CustomList.item_state`)
    queued: True if the item's state has changed but this is not yet reflected
            in `state`

"""

    # runs getstate for all items
    _refresh = qt.pyqtSignal()

    def __init__ (self, types, add_tooltip, rm_tooltip):
        Dynamic.__init__(self)
        Changing.__init__(self)
        qt.QVBoxLayout.__init__(self)

        self._new_btn = new_type = qt.QComboBox()
        self.addWidget(new_type)
        # https://bugreports.qt.io/browse/QTBUG-40807
        add_type = lambda i: self.add(new_type.itemData(i))
        new_type.activated.connect(util.rate_limit(0.1, add_type))

        widgets.add_combobox_items(new_type, *(
            {
                'icon': 'list-add',
                qt.Qt.UserRole: data['id'],
                qt.Qt.DisplayRole: data['name'],
                qt.Qt.ToolTipRole: data['description']
            } for data in types
        ))

        self.types = {data['id']: data for data in types}
        self._rm_tooltip = rm_tooltip
        self.items = []

    @staticmethod
    def item_state (item, interrupt=None):
        """Get the state of an item, using its type's `getstate` function.

interrupt: abort when this function returns True

May block for a long time, so shouldn't be called in the UI thread.

"""
        if item['queued']:
            get_state = item['getstate']
            item['state'] = (None if get_state is None
                             else get_state(item['data'], interrupt))
        return item['state']

    def add (self, item_type):
        """Add an item of the given type (`id`).

Returns the item, as added to `CustomList.items`.

"""
        if item_type not in self.types:
            raise ValueError(item_type)
        item = None

        rm_btn = widgets.mk_button(qt.QPushButton, {
            'icon': 'list-remove',
            'tooltip': self._rm_tooltip,
            'clicked': lambda: self.rm(item)
        })

        def changed ():
            item['queued'] = True
            self.changed.emit()

        defn = self.types[item_type]
        header = widgets.mk_label('<b>{}</b>'.format(escape(defn['name'])),
                                  rich=True, tooltip=defn['description'])
        header.setContentsMargins(5, 5, 5, 5)
        result = defn['create'](changed)
        controls = result['item']

        layout = qt.QGridLayout()
        layout.setColumnStretch(0, 1)
        layout.addWidget(header, 0, 0)
        (layout.addLayout if isinstance(controls, qt.QLayout)
         else layout.addWidget)(controls, 1, 0, 1, 2, qt.Qt.AlignTop)
        layout.addWidget(rm_btn, 0, 1)
        widget = widgets.widget_from_layout(layout)
        if 'doc' in defn:
            widget.setWhatsThis(defn['doc'])
        self.insertWidget(len(self.items), widget)

        # focus specific widget, or the main one, or any we can find
        focus = result.get('focus')
        if isinstance(focus, qt.QWidget):
            pass
        elif isinstance(controls, qt.QWidget):
            focus = controls
        else:
            focus = widgets.first_layout_widget(controls)
        if focus is not None:
            focus.setFocus(qt.Qt.OtherFocusReason)
            # for text boxes, also select all text
            if isinstance(focus,
                          (qt.QLineEdit, qt.QTextEdit, qt.QPlainTextEdit)):
                focus.selectAll()

        item = {
            'type': item_type,
            'data': result['data'],
            'item': widget,
            'changed': changed,
            'getstate': self.types[item_type].get('getstate'),
            'queued': False
        }

        self.items.append(item)
        self._refresh.connect(changed)
        changed()
        self.new_widget.emit()
        return item

    def rm (self, item):
        """Remove the given item (from `CustomList.items`)."""
        self.items.remove(item)
        self.removeWidget(item['item'])
        item['item'].deleteLater()
        self._refresh.disconnect(item['changed'])
        self.changed.emit()
        self.new_widget.emit()
        self._new_btn.setFocus(qt.Qt.OtherFocusReason)

    def refresh (self):
        """Run the `getstate` function for all items."""
        self._refresh.emit()

class FilesSection (CustomList):
    """UI section for selecting input paths.

options: OptionsSection

Item states are dicts with 'input' being a `core.inputs.Input`.

"""

    # NOTE: UI section heading
    name = _('Files')
    doc = doc.files_section

    def __init__ (self, options):
        CustomList.__init__(self, [
            {
                'id': 'glob',
                # NOTE: file source type name
                'name': _('Pattern'),
                'description': _('Use a glob-style pattern'),
                'create': self._new_glob,
                'getstate': self._get_glob_state,
                'doc': doc.files_glob
            }, {
                'id': 'list',
                # NOTE: file source type name
                'name': _('List'),
                'description': _('Specify files manually'),
                'create': self._new_list,
                'getstate': self._get_list_state,
                'doc': doc.files_list
            }, {
                'id': 'recursive',
                # NOTE: file source type name
                'name': _('Recursive Files'),
                'description': _(
                    'Specify a directory in which to find files recursively'
                ),
                'create': self._new_recursive,
                'getstate': self._get_recursive_state,
                'doc': doc.files_recursive
            }
        ], _('Add a source of files'), _('Remove this source of files'))

        self._options = options
        options.changed.connect(self.refresh)
        # if None, should use cwd
        self._last_file_browser_dir_value = None

    @property
    def inputs (self):
        """Sequence of `inputs.Input`."""
        return [CustomList.item_state(f)['input'] for f in self.items]

    @property
    def _last_file_browser_dir (self):
        return (self._options.cwd if self._last_file_browser_dir_value is None
                else self._last_file_browser_dir_value)

    @_last_file_browser_dir.setter
    def _last_file_browser_dir (self, d):
        self._last_file_browser_dir_value = d

    # add paths with a new list item
    def add_paths (self, paths):
        if paths:
            item = self.add('list')
            item['data']['add_paths'](
                optimise_paths(self._last_file_browser_dir, paths))

    def _get_list_state (self, data, interrupt):
        paths = filter(lambda path: path,
                       data['text_widget'].toPlainText().splitlines())
        return {'input': core.inputs.StaticInput(*paths)}

    # show a file dialogue and pass files to the given function
    def _list_browse (self, add_paths):
        # can't use a static function since we need the current dir afterwards
        fd_dir = self._last_file_browser_dir
        fd = qt.QFileDialog(directory=fd_dir)
        fd.setFileMode(qt.QFileDialog.ExistingFiles)
        fd.setOptions(qt.QFileDialog.HideNameFilterDetails)

        if fd.exec():
            files = fd.selectedFiles()
            add_paths(files)
            self._last_file_browser_dir = fd.directory().absolutePath()

    def _new_list (self, changed):
        layout = qt.QVBoxLayout()

        text = qt.QPlainTextEdit()
        layout.addWidget(text)
        text.setLineWrapMode(qt.QPlainTextEdit.NoWrap)
        widgets.set_text_desc(text, _('List of files, one per line'))
        text.textChanged.connect(changed)

        # append given iterable of paths to the list widget
        def add_paths (paths):
            text.appendPlainText('\n'.join(paths) + '\n')
            text.setFocus(qt.Qt.OtherFocusReason)

        browse = widgets.mk_button(qt.QPushButton, {
            # NOTE: label for a button that opens a file browser dialogue
            'text': _('Browse...'),
            'icon': 'document-open',
            'clicked': lambda: self._list_browse(add_paths)
        })
        layout.addWidget(browse)

        return {
            'data': {'text_widget': text, 'add_paths': add_paths},
            'item': layout
        }

    def _get_glob_state (self, data, interrupt):
        inp = core.inputs.GlobInput(
            data['text_widget'].text(), self._options.cwd)
        return {'input': inp}

    def _new_glob (self, changed):
        text = qt.QLineEdit()
        widgets.set_text_desc(text, _('Glob-style pattern'))
        # NOTE: default value for the 'Pattern' file source; 'ext' means file
        # extension
        text.setText(_('*.ext'))
        text.textChanged.connect(changed)
        return {'data': {'text_widget': text}, 'item': text}

    def _get_recursive_state (self, data, interrupt):
        path = data['text_widget'].text()
        return {
            'input': core.inputs.RecursiveFilesInput(path, self._options.cwd)
        }

    def _new_recursive (self, changed):
        text = qt.QLineEdit()
        widgets.set_text_desc(text, _('Directory path'))
        text.textChanged.connect(changed)
        return {'data': {'text_widget': text}, 'item': text}

class FieldsSection (CustomList):
    """UI section for defining fields.

Item states are dicts with 'fields' being a `core.field.Fields`.

files: `FilesSection`

"""

    # NOTE: UI section heading
    name = _('Fields')
    doc = doc.fields_section

    def __init__ (self, files):
        CustomList.__init__(self, [
            {
                'id': 'component',
                # NOTE: field source type name
                'name': _('Path Component'),
                'description':
                    _('Use the filename or a directory from the path'),
                'create': self._new_component,
                'getstate': self._get_component_state,
                'doc': doc.fields_component
            }, {
                'id': 'regex',
                # NOTE: field source type name
                'name': _('Regular Expression'),
                'description': 'Capture groups from a matching regex',
                'create': self._new_regex,
                'getstate': self._get_regex_state,
                'doc': doc.fields_regex
            }, {
                'id': 'ordering',
                # NOTE: field source type name
                'name': _('Ordering'),
                'description': _('Use the position when the files are ordered'),
                'create': self._new_ordering,
                'getstate': self._get_ordering_state,
                'doc': doc.fields_ordering
            }
        ], _('Add a source of fields'), _('Remove this source of fields'))
        self._files = files
        files.changed.connect(self.refresh)

    def names (self):
        """Get a set of the names of all defined fields."""
        return set(sum((CustomList.item_state(item)['fields'].names
                        for item in self.items), []))

    def _get_component_state (self, data, interrupt):
        return {'fields': core.field.PathComponent(
            field_name=data['field_widget'].text(),
            index=data['index_widget'].text()
        )}

    def _new_component (self, changed):
        layout = qt.QHBoxLayout()

        idx = qt.QLineEdit()
        layout.addWidget(idx)
        idx.setText('-1')
        widgets.set_text_desc(idx, _('Path component index'))
        idx.textChanged.connect(changed)
        field = qt.QLineEdit()
        layout.addWidget(field)
        # NOTE: default value for the field name for the 'Path Component' field
        # source
        field.setText(_('name'))
        widgets.set_text_desc(field, _('Field name'))
        field.textChanged.connect(changed)

        return {'data': {
            'index_widget': idx,
            'field_widget': field
        }, 'item': layout, 'focus': idx}

    def _get_regex_state (self, data, interrupt):
        return {'fields': core.field.RegexGroups(
            pattern=data['text_widget'].text(),
            field_name_prefix=data['field_widget'].text(),
            context=data['context_widget'].context
        )}

    def _new_regex (self, changed):
        layout = qt.QGridLayout()

        text = qt.QLineEdit()
        layout.addWidget(text, 0, 0, 1, 2)
        # NOTE: default value for the 'Regular Expression' field source; 'ext'
        # means file extension
        text.setText(_('(?P<name>.*)\.(?P<ext>[^.]*)'))
        text.textChanged.connect(changed)

        context = FieldContext()
        layout.addWidget(context, 1, 0, 1, 1)
        context.currentIndexChanged.connect(changed)
        field = qt.QLineEdit()
        layout.addWidget(field, 1, 1, 1, 1)
        # NOTE: default value for the field name for the 'Path Component' field
        # source
        field.setText(_('group'))
        widgets.set_text_desc(field, _('Field name'))
        field.textChanged.connect(changed)

        return {'data': {
            'text_widget': text,
            'context_widget': context,
            'field_widget': field
        }, 'item': layout, 'focus': text}

    def _get_ordering_state (self, data, interrupt):
        base_key = data['sorttype_widget'].currentData()
        key = (
            base_key
            if data['casesensitive_widget'].isChecked()
            else lambda s: base_key(s.lower())
        )

        return {'fields': core.field.Ordering(
            field_name=data['field_widget'].text(),
            key=key,
            reverse=not data['ascending_widget'].isChecked(),
            context=data['context_widget'].context,
            fmt=data['pad'].get_fmt(interrupt)
        )}

    def _new_ordering (self, changed):
        layout = qt.QGridLayout()

        sort_type = qt.QComboBox()
        widgets.add_combobox_items(sort_type, *(
            {
                qt.Qt.UserRole: sort_type,
                qt.Qt.DisplayRole: sort_type.name,
                qt.Qt.ToolTipRole: sort_type.desc
            } for sort_type in core.field.all_sort_types
        ))
        sort_type.currentIndexChanged.connect(changed)
        layout.addWidget(sort_type, 0, 0)

        case_sensitive = qt.QCheckBox(_('Case-sensitive'))
        layout.addWidget(case_sensitive, 0, 1)
        case_sensitive.stateChanged.connect(changed)
        # NOTE: sort order
        ascending = qt.QCheckBox(_('Ascending'))
        layout.addWidget(ascending, 0, 2)
        ascending.setChecked(True)
        ascending.stateChanged.connect(changed)

        context = FieldContext()
        layout.addWidget(context, 1, 0, 1, 1)
        context.currentIndexChanged.connect(changed)
        field = qt.QLineEdit()
        layout.addWidget(field, 1, 1, 1, 2)
        # NOTE: default value for the field name for the 'Ordering' field source
        field.setText(_('position'))
        widgets.set_text_desc(field, _('Field name'))
        field.textChanged.connect(changed)

        pad = OrderingPadding(self._files, changed)
        pad.new_widget.connect(self.new_widget.emit)
        layout.addLayout(pad, 2, 0, 1, 2)

        return {'data': {
            'sorttype_widget': sort_type,
            'casesensitive_widget': case_sensitive,
            'ascending_widget': ascending,
            'context_widget': context,
            'field_widget': field,
            'pad': pad
        }, 'item': layout, 'focus': field}

class TemplateSection (Changing, qt.QVBoxLayout):
    """UI section for defining the output path template.

Attributes:

template: `string.Template`

"""

    # NOTE: UI section heading
    name = _('Output Template')
    doc = doc.template

    def __init__ (self):
        Changing.__init__(self)
        qt.QVBoxLayout.__init__(self)

        def changed_text (text):
            self.template = string.Template(text)
            self.changed.emit()

        text = qt.QLineEdit()
        self.addWidget(text)
        widgets.set_text_desc(text, _('Destination path template'))
        text.textChanged.connect(changed_text)
        changed_text('')

class OptionsSection (Changing, qt.QFormLayout):
    """UI section for setting options."""

    # NOTE: UI section heading
    name = _('Options')

    def __init__ (self):
        qt.QFormLayout.__init__(self)

        self._cwd = cwd = widgets.DirButton(os.getcwd())
        self.addRow(_('Working &directory:'), cwd)
        cwd.setWhatsThis(doc.cwd_section)
        cwd.changed.connect(self.changed.emit)

        # NOTE: checkbox label for option to copy files instead of renaming
        self._copy = copy = qt.QCheckBox(_('Copy files'))
        copy.setToolTip(_('Instead of renaming items, copy them (slower)'))
        copy.setChecked(False)
        # NOTE: heading for a set of options
        self.addRow(_('When running:'), copy)

    @property
    def cwd (self):
        """Path for the current working directory to use for relative paths."""
        return self._cwd.path

    @property
    def copy (self):
        """Whether to copy files instead of renaming."""
        return self._copy.isChecked()

class Input (qt.QScrollArea):
    """Section of the UI with controls for defining the renaming scheme.

new_widget: function to call when widgets are added
changed: function to call when any settings change

Attributes:

files: FilesSection
fields: FieldsSection
template: TemplateSection
options: OptionsSection

"""

    def __init__ (self, new_widget, changed):
        qt.QScrollArea.__init__(self)
        self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
        self.setWidgetResizable(True)
        self.setFrameShape(qt.QFrame.NoFrame)

        self.options = OptionsSection()
        self.options.changed.connect(changed)
        self.files = FilesSection(self.options)
        self.files.new_widget.connect(new_widget)
        self.files.changed.connect(changed)
        self.fields = FieldsSection(self.files)
        self.fields.new_widget.connect(new_widget)
        self.fields.changed.connect(changed)
        self.template = TemplateSection()
        self.template.changed.connect(changed)

        self._layout = qt.QVBoxLayout()
        self.setWidget(widgets.widget_from_layout(self._layout))
        self._layout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
        self.add_section(self.files)
        self.add_section(self.fields)
        self.add_section(self.template)
        self.add_section(self.options)

    def add_section (self, section):
        group = qt.QGroupBox(section.name)
        if hasattr(section, 'doc'):
            group.setWhatsThis(section.doc)
        group.setLayout(section)
        self._layout.addWidget(group)

    def gather (self, interrupt=None):
        """Return data defining the renaming scheme.

interrupt: abort when this function returns True

May block for a long time, so shouldn't be called in the UI thread.

Returns `(inps, fields, template, cwd)`, where:

inps: sequence of `core.inputs.Input`
fields: sequence of `core.inputs.Fields`
template: `string.Template` for the output path
options: `OptionsSection`

"""
        field_sets = []
        for f in self.fields.items:
            field_sets.append(CustomList.item_state(f, interrupt)['fields'])

        return (
            self.files.inputs,
            field_sets,
            self.template.template,
            self.options
        )


class Ammo:
    def __init__(self,name,price,kind):
        self.name = name
        self.kind = kind
        self.price = price

class Weapons:
    def __init__(self,name,price,damage,size,flag,_range,kind):
        self.name = name
        self.price = price
        self.isMartial = flag
        self.dice = damage
        self.size = size
        self.range = _range
        self.kind = kind

class Ranged(Weapons):
    def __init__(self,name,price,damage,size,flag,kind,_range,ammotype):
        super().__init__(name,price,damage,size,flag,_range,kind)
        self.ammo = ammotype
    def show(self):
        print("\t",self.name,"["+self.__class__.__name__,":",self.kind+"]")
        print("\t\tDamage:",self.dice,"\tRange:",self.range,"\tAmmo:",self.ammo.name)

class Melee(Weapons):
    def __init__(self,name,price,damage,size,flag,kind,_range):
        super().__init__(name,price,damage,size,flag,_range,kind)
    def show(self):
        print("\t",self.name,"["+self.__class__.__name__,":",self.kind+"]")
        print("\t\tDamage:",self.dice,"\tRange:",self.range)

class Throw(Weapons):
    def __init__(self,name,price,damage,size,flag,kind,_range,influence):
        super().__init__(name,price,damage,size,flag,_range,kind)
        self.piece = 0
        self.influence = influence
    def change(self,mod,piece):
        self.piece+=piece*mod
    def show(self):
        print("\t",self.name,"["+self.__class__.__name__,":",self.kind+"]")
        print("\t\tDamage:",self.dice,"\tRange:",self.range)
        print("\t\t"+self.influence,"Effect\tPiece:",self.piece)

ammos = {
    "Arrow":Ammo("Arrow",25,"Bow"),
    "Pebble":Ammo("Pebble",5,"Sling"),
}
weapons = {
    "Short Bow":Ranged("Short Bow",2500,"d6",1,False,"Bow",5,ammos["Arrow"]),
    "Long Bow":Ranged("Long Bow",7500,"2d8",3,True,"Bow",10,ammos["Arrow"]),
    "Crossbow":Ranged("Crossbow",5000,"d10",2,False,"Bow",8,ammos["Arrow"]),
    "Boomerang":Ranged("Boomerang",6000,"d8",2,True,"Boomerang",4,0),
    "Sling":Ranged("Sling",1000,"d4",1,True,"Sling",5,ammos["Pebble"]),
    "Dagger":Melee("Dagger",1000,"d4",1,False,"Dagger",0),
    "Knuckleduster":Melee("Knuckleduster",10000,"d20",1,True,"Knuckleduster",0),
    "Quarterstaff":Melee("Quarterstaff",1000,"2d4",1,False,"Staff",1),
    "Short Sword":Melee("Short Sword",1200,"d6",1,False,"Sword",1),
    "Falx":Melee("Falx",1200,"2d6",1,False,"Sword",1),
    "Longsword":Melee("Longsword",2000,"d10",2,False,"Sword",2),
    "Axe":Melee("Axe",2000,"d10",1,False,"Axe",1),
    "Mace":Melee("Mace",2500,"d12",1,True,"Mace",1),
    "Spear":Melee("Spear",500,"d6",2,False,"Spear",3),
    "Warhammer":Melee("Warhammer",7500,"2d10",3,True,"Hammer",1),
    "Greatsword":Melee("Greatsword",5000,"2d12",3,True,"Sword",2),
    "Arrow":Ammo("Arrow",25,"Bow"),
    "Pebble":Ammo("Pebble",5,"Sling"),
    "Bomb":Throw("Bomb",500,"d8",2,True,"Bomb",3,"Area"),
    "Knife":Throw("Knife",50,"d4",1,False,"Knife",3,"Spot"),
    "Javelin":Throw("Javelin",100,"d6",3,False,"Javelin",4,"Spot")
}


from flask_restful import Resource
from flask import request, json, session
from middleware import auth
from .validators import valid_add_label, valid_delete_label
from .models import Label

class AddLabel(Resource):
    method_decorators = {'post': [auth.login_required]}

    def post(self):
        """
            This API is used to create a label
            @param request: label name
            @return: Creates label
        """
        req_data = request.data
        body = json.loads(req_data)
        body['user_id'] = session['user_id']
        valid_data = valid_add_label(body)
        if valid_data:
            return {'data': valid_data, 'code': 500}
        lb = Label(**body)
        lb.save()
        return {'message': 'label added', 'code': 200}

class DeleteLabel(Resource):
    method_decorators = {'delete': [auth.login_required]}

    def delete(self, label):
        """
            This API is used to delete existing label
            @param label_id: primary_key of the specific label
            @return: delete the label
        """
        valid_data = valid_delete_label(label)
        user_id = session['user_id']
        if valid_data:
            return valid_data
        lb = Label.objects.filter(user_id=user_id, label=label).first()
        lb.delete()
        return {'message': 'label deleted', 'code': 200}