text:
using RayTracer: FixedParams, @diffops
import Base: +, *, -

struct DoneRewardInfo
    done::Bool
    done_why::String
    reward::Float32
    done_code::String
end

# Randomization code

# Rendering window size
WINDOW_WIDTH = 800
WINDOW_HEIGHT = 600

# Camera image size
DEFAULT_CAMERA_WIDTH = 640
DEFAULT_CAMERA_HEIGHT = 480

# Blue sky horizon color
BLUE_SKY_COLOR = Vec3([0.45f0], [0.82f0], [1f0])

# Color meant to approximate interior walls
WALL_COLOR = Vec3([0.64f0], [0.71f0], [0.28f0])

# Ground/floor color
GROUND_COLOR = Vec3([0.15f0])

# Angle at which the camera is pitched downwards
CAMERA_ANGLE = 20f0

# Camera field of view angle in the Y direction
# Note: robot uses Raspberri Pi camera module V1.3
# https://www.raspberrypi.org/documentation/hardware/camera/README.md
CAMERA_FOV_Y = 42f0

# Distance from camera to floor (10.8cm)
CAMERA_FLOOR_DIST = 0.108f0

# Forward distance between the camera (at the front)
# and the center of rotation (6.6cm)
CAMERA_FORWARD_DIST = 0.066f0

# Distance (diameter) between the center of the robot wheels (10.2cm)
WHEEL_DIST = 0.102f0

# Total robot width at wheel base, used for collision detection
# Note: the actual robot width is 13cm, but we add a litte bit of buffer
#       to faciliate sim-to-real transfer.
ROBOT_WIDTH = 0.13f0 + 0.02f0

# Total robot length
# Note: the center of rotation (between the wheels) is not at the
#       geometric center see CAMERA_FORWARD_DIST
ROBOT_LENGTH = 0.18f0

# Height of the robot, used for scaling
ROBOT_HEIGHT = 0.12f0

# Safety radius multiplier
SAFETY_RAD_MULT = 1.8f0

# Robot safety circle radius
AGENT_SAFETY_RAD = (max(ROBOT_LENGTH, ROBOT_WIDTH) / 2) * SAFETY_RAD_MULT

# Minimum distance spawn position needs to be from all objects
MIN_SPAWN_OBJ_DIST = 0.25f0

# Road tile dimensions (2ft x 2ft, 61cm wide)
# self.road_tile_size = 0.61

# Maximum forward robot speed in meters/second
DEFAULT_ROBOT_SPEED = 1.2f0
# approx 2 tiles/second

DEFAULT_FRAMERATE = 30

DEFAULT_MAX_STEPS = 1500

DEFAULT_MAP_NAME = "udem1"

DEFAULT_FRAME_SKIP = 1

DEFAULT_ACCEPT_START_ANGLE_DEG = 60

REWARD_INVALID_POSE = -1000f0

MAX_SPAWN_ATTEMPTS = 5000

struct LanePosition
    dist
    dot_dir
    angle_deg
    angle_rad
end

function as_json_dict(lp::LanePosition)
    ### Serialization-friendly format. ###
    return Dict([:dist=>lp.dist,
                 :dot_dir=>lp.dot_dir,
                 :angle_deg=>lp.angle_deg,
                 :angle_rad=>lp.angle_rad])
end

# Raised when the Duckiebot is not in a lane. #
struct NotInLane <: Exception end

mutable struct Simulator
    ##
    #Simple road simulator to test RL training.
    #Draws a road with turns using RayTracer, and simulates
    #basic differential-drive dynamics.
    ##
    last_action::Vector{Float32}
    wheelVels::Vector{Float32}
    speed::Float32
    cur_pos::Union{Vector{Float32}, Nothing}
    cur_angle::Union{Float32, Nothing}
    step_count::Int
    timestamp::Float32
    fixedparams::FixedSimParams
    scene::Vector{Triangle}
    done::Bool
end

@diffops Simulator

function Simulator(; map_name::String=DEFAULT_MAP_NAME, max_steps::Int=DEFAULT_MAX_STEPS, kwargs...)
    fp = FixedSimParams(map_name, max_steps; kwargs...)
    cur_pos, cur_angle = reset!(fp)

    last_action = zeros(Float32, 2)
    wheelVels = zeros(Float32, 2)

    step_count = 0
    timestamp = 0f0

    # Robot's current speed
    speed = 0f0

    done = false

    scene = _render_img(fp, cur_pos, cur_angle, false)[1]
    sim = Simulator(last_action, wheelVels, speed, cur_pos, cur_angle,
                    step_count, timestamp, fp, scene, done)

    sim.done = _compute_done_reward(sim).done
    return sim
end

function Base.display(io::IO, sim::Simulator)
    print("Simulator($(sim.fixedparams._map.map_name))")
end

function reset!(sim::Simulator)
    ##
    #Reset the simulation at the start of a new episode
    #This also randomizes many environment parameters (domain randomization)
    ##

    sim.cur_pos, sim.cur_angle = reset!(sim.fixedparams)

    sim.last_action = zeros(Float32, 2)
    sim.wheelVels = zeros(Float32, 2)

    sim.step_count = 0
    sim.timestamp = 0f0

    # Robot's current speed
    sim.speed = 0f0

    sim.done = _compute_done_reward(sim).done
    # Generate the first camera image
    obs = render_obs(sim)
end

_objects(grid::Grid) = grid.obj_data.objects
_objects(map::Map) = _objects(map._grid)
_objects(fp::FixedSimParams) = _objects(fp._map)
_objects(sim::Simulator) = _objects(sim.fixedparams)

_grid(map::Map) = map._grid
_grid(fp::FixedSimParams) = _grid(fp._map)
_grid(sim::Simulator) = _grid(sim.fixedparams)

_drivable_tiles(map::Map) = map._grid.drivable_tiles
_drivable_tiles(fp::FixedSimParams) = _drivable_tiles(fp._map)
_drivable_tiles(sim::Simulator) = _drivable_tiles(sim.fixedparams)

_start_tile(map::Map) = map._grid.start_tile
_start_tile(fp::FixedSimParams) = _start_tile(fp._map)
_start_tile(sim::Simulator) = _start_tile(sim.fixedparams)

_road_tile_size(grid::Grid) = grid.road_tile_size
_road_tile_size(map::Map) = _road_tile_size(map._grid)
_road_tile_size(fp::FixedSimParams) = _road_tile_size(fp._map)
_road_tile_size(sim::Simulator) = _road_tile_size(sim.fixedparams)

_collidable_corners(grid::Grid) = grid.obj_data.collidable_corners
_collidable_corners(map::Map) = _collidable_corners(map._grid)
_collidable_corners(fp::FixedSimParams) = _collidable_corners(fp._map)
_collidable_corners(sim::Simulator) = _collidable_corners(sim.fixedparams)

_collidable_norms(grid::Grid) = grid.obj_data.collidable_norms
_collidable_norms(map::Map) = _collidable_norms(map._grid)
_collidable_norms(fp::FixedSimParams) = _collidable_norms(fp._map)
_collidable_norms(sim::Simulator) = _collidable_norms(sim.fixedparams)

_collidable_centers(grid::Grid) = grid.obj_data.collidable_centers
_collidable_centers(map::Map) = _collidable_centers(map._grid)
_collidable_centers(fp::FixedSimParams) = _collidable_centers(fp._map)
_collidable_centers(sim::Simulator) = _collidable_centers(sim.fixedparams)

_collidable_safety_radii(grid::Grid) = grid.obj_data.collidable_safety_radii
_collidable_safety_radii(map::Map) = _collidable_safety_radii(map._grid)
_collidable_safety_radii(fp::FixedSimParams) = _collidable_safety_radii(fp._map)
_collidable_safety_radii(sim::Simulator) = _collidable_safety_radii(sim.fixedparams)

_delta_time(fp::FixedSimParams) = fp.delta_time
_delta_time(sim::Simulator) = _delta_time(sim.fixedparams)

_robot_speed(fp::FixedSimParams) = fp.robot_speed
_robot_speed(sim::Simulator) = _robot_speed(sim.fixedparams)

_max_steps(fp::FixedSimParams) = fp.max_steps
_max_steps(sim::Simulator) = _max_steps(sim.fixedparams)

_wheel_dist(fp::FixedSimParams) = fp.wheel_dist
_wheel_dist(sim::Simulator) = _wheel_dist(sim.fixedparams)

_full_transparency(fp::FixedSimParams) = fp.full_transparency
_full_transparency(sim::Simulator) = _full_transparency(sim.fixedparams)

function closest_curve_point(fp::FixedSimParams, pos, angle)
    ##
    #    Get the closest point on the curve to a given point
    #    Also returns the tangent at that point.
    #
    #    Returns nothing, nothing if not in a lane.
    ##

    i, j = get_grid_coords(_road_tile_size(fp), pos)
    tile = _get_tile(_grid(fp), i, j)

    (isnothing(tile) || !tile["drivable"]) && return nothing, nothing

    # Find curve with largest dotproduct with heading
    curves = _get_tile(_grid(fp), i, j)["curves"]
    curve_headings = permutedims(curves[end, :, :] .- curves[1, :, :])
    curve_headings = Float32.(curve_headings / norm(curve_headings))
    dir_vec = get_dir_vec(angle)

    dot_prods = map(i -> dot(curve_headings[i, :], dir_vec), 1:size(curve_headings, 1))

    # Closest curve = one with largest dotprod
    max_idx = argmax(dot_prods)
    cps = curves[:, :, max_idx]
    # Find closest point and tangent to this curve
    t = bezier_closest(cps, pos)
    point = bezier_point(cps, t)
    tangent = bezier_tangent(cps, t)

    return point, tangent
end

# This is a hack to avoid using try-catch and NotInLane errors
is_inlane(lp::LanePosition) = !(lp.dot_dir == lp.dist == lp.angle_deg == lp.angle_rad == 0f0)

function get_lane_pos2(fp::FixedSimParams, pos, angle)
    ##
    #Get the position of the agent relative to the center of the right lane
    #
    #Raises NotInLane if the Duckiebot is not in a lane.
    ##

    # Get the closest point along the right lane's Bezier curve,
    # and the tangent at that point
    point, tangent = closest_curve_point(fp, pos, angle)
    if isnothing(point)
        msg = "Point not in lane: $pos"
        # throw(NotInLane(msg))
        return LanePosition(0f0, 0f0, 0f0, 0f0)
    end

    @assert !isnothing(point)

    # Compute the alignment of the agent direction with the curve tangent
    dirVec = get_dir_vec(angle)
    dotDir = dot(dirVec, tangent)
    dotDir = max(-1f0, min(1f0, dotDir))

    # Compute the signed distance to the curve
    # Right of the curve is negative, left is positive
    posVec = pos .- point
    upVec = [0f0, 1f0, 0f0]
    rightVec = cross(tangent, upVec)
    signedDist = dot(posVec, rightVec)

    # Compute the signed angle between the direction and curve tangent
    # Right of the tangent is negative, left is positive
    angle_rad = acos(dotDir)

    if dot(dirVec, rightVec) < 0
        angle_rad *= -1f0
    end

    angle_deg = rad2deg(angle_rad)

    return LanePosition(signedDist, dotDir, angle_deg, angle_rad)
end

function _drivable_pos(grid::Grid, pos)
    ##
    #Check that the given (x,y,z) position is on a drivable tile
    ##

    coords = get_grid_coords(grid.road_tile_size, pos)
    tile = _get_tile(grid, coords...)
    if isnothing(tile)
        msg = "No tile found at $pos $coords"
        #logger.debug(msg)
        return false
    end

    if !tile["drivable"]
        msg = "$pos corresponds to tile at $coords which is not drivable: $tile"
        #logger.debug(msg)
        return false
    end

    return true
end

function Base.show(io::IO, sim::Simulator)
    map_name = sim.fixedparams._map.map_name
    print("Simulator($map_name)")
end

function close(sim::Simulator) end

function _collidable_object(grid, grid_width, grid_height, obj_corners::Array{Float32,2}, obj_norm,
                            possible_tiles, road_tile_size::Float32)
    ##
    #A function to check if an object intersects with any
    #drivable tiles, which would mean our agent could run into them.
    #Helps optimize collision checking with agent during runtime
    ##

    size(possible_tiles) == (0,) && (return false)
    drivable_tiles = Vector{NTuple{2,Int}}()
    for c in possible_tiles
        tile = _get_tile(grid, c..., grid_width, grid_height)
        if !ismissing(tile) && !isnothing(tile) && tile["drivable"]
            drivable_tiles = vcat(drivable_tiles, [Tuple(c)])
        end
    end
    isempty(drivable_tiles) && (return false)

    # Tiles are axis aligned, so add normal vectors in bulk
    tile_norms = repeat([1f0 0f0; 0f0 1f0],  length(drivable_tiles))

    # Find the corners for each candidate tile
    drivable_tiles = map(pt->permutedims(
        tile_corners(
                _get_tile(grid, pt..., grid_width, grid_height)["coords"],
                road_tile_size
        )), drivable_tiles)

    # Stack doesn't do anything if there's only one object,
    # So we add an extra dimension to avoid shape errors later
    if ndims(tile_norms) == 2
        tile_norms = [tile_norms]
    end
    # Only add it if one of the vertices is on a drivable tile

    return intersects(obj_corners, drivable_tiles, obj_norm, tile_norms)
end

function get_grid_coords(road_tile_size, abs_pos)
    ##
    #Compute the tile indices (i,j) for a given (x,_,z) world position
    #
    #x-axis maps to increasing i indices
    #z-axis maps to increasing j indices
    #
    #Note: may return coordinates outside of the grid if the
    #position entered is outside of the grid.
    ##

    x, _, z = abs_pos
    i = floor(x / road_tile_size)
    j = floor(z / road_tile_size)

    return Int.((i, j)) .+ 1
end

get_dir_vec(sim::Simulator) = get_dir_vec(sim.cur_angle)
get_dir_vec(sim::Simulator, ::Nothing) = get_dir_vec(sim.cur_angle)

function get_dir_vec(angle)
    ##
    #Vector pointing in the direction the agent is looking
    ##
    x = cos(angle)
    z = -sin(angle)
    return [x, 0f0, z]
end

get_right_vec(sim::Simulator) = get_right_vec(sim.cur_angle)
get_right_vec(sim::Simulator, ::Nothing) = get_right_vec(sim.cur_angle)

function get_right_vec(angle)
    ##
    #Vector pointing to the right of the agent
    ##

    x = sin(angle)
    z = cos(angle)
    return [x, 0f0, z]
end

function _proximity_penalty2(sim::Simulator, pos, angle)
    ##
    #Calculates a 'safe driving penalty' (used as negative rew.)
    #as described in Issue #24 of gym-duckietown
    #
    #Describes the amount of overlap between the "safety circles" (circles
    #that extend further out than BBoxes, giving an earlier collision 'signal'
    #The number is max(0, prox.penalty), where a lower (more negative) penalty
    #means that more of the circles are overlapping
    ##

    pos = _actual_center(pos, angle)
    if length(_collidable_centers(sim)) == 0
        static_dist = 0
    # Find safety penalty w.r.t static obstacles
    else
        d = Float32.(norm.(_collidable_centers(sim) .- [pos]))

        if !safety_circle_intersection(d, AGENT_SAFETY_RAD, _collidable_safety_radii(sim))
            static_dist = 0
        else
            static_dist = safety_circle_overlap(d, AGENT_SAFETY_RAD, _collidable_safety_radii(sim))
        end
    end
    total_safety_pen = static_dist
    for obj in _objects(sim)
        # Find safety penalty w.r.t dynamic obstacles
        total_safety_pen = total_safety_pen + proximity(obj, pos, AGENT_SAFETY_RAD)
    end
    return total_safety_pen
end

update_physics(sim::Simulator, action) = update_physics(sim, action, _delta_time(sim))
update_physics(sim::Simulator, action, ::Nothing) = update_physics(sim, action, _delta_time(sim))

function update_physics(sim::Simulator, action, delta_time)
    sim.wheelVels = action * _robot_speed(sim)
    prev_pos = sim.cur_pos

    # Update the robot's position

    if _valid_pose(sim.fixedparams, sim.cur_pos, sim.cur_angle)
        sim.cur_pos, sim.cur_angle = _update_pos(sim.cur_pos,
                                                 sim.cur_angle,
                                                 _wheel_dist(sim),
                                                 sim.wheelVels,
                                                 delta_time)
    end

    sim.step_count += 1
    sim.timestamp += delta_time

    sim.last_action = action

    # Compute the robot's speed
    delta_pos = sim.cur_pos .- prev_pos
    sim.speed = norm(delta_pos) / delta_time

    # Update world objects
    objs = _objects(sim)
    for obj in objs
        if !_static(obj) && _kind(obj) == "duckiebot"
            obj_i, obj_j = get_grid_coords(_road_tile_size(sim), _pos(obj))
            cond(o) = get_grid_coords(_road_tile_size(sim), _pos(obj)) == (obj_i, obj_j) && o != obj
            same_tile_obj = filter(cond, objs)
            step!(obj, delta_time, sim.fixedparams, closest_curve_point, same_tile_obj)
        else
            step!(obj, delta_time)
        end
    end
end

function get_agent_info(sim::Simulator)
    info = Dict()
    pos = sim.cur_pos
    angle = sim.cur_angle
    # Get the position relative to the right lane tangent

    info["action"] = vcat(sim.last_action)
    if _full_transparency(sim)
        #             info['desc'] = """
        #
        # cur_pos, cur_angle ::  simulator frame (non cartesian)
        #
        # egovehicle_pose_cartesian :: cartesian frame
        #
        #     the map goes from (0,0) to (grid_height, grid_width)*sim.road_tile_size
        #
        # """
        try
            lp = get_lane_pos2(sim.fixedparams, pos, angle)
            info["lane_position"] = as_json_dict(lp)
        catch y end

        info["robot_speed"] = sim.speed
        info["proximity_penalty"] = _proximity_penalty2(sim, pos, angle)
        info["cur_pos"] = float.(pos)
        info["cur_angle"] = float(angle)
        info["wheel_velocities"] = [sim.wheelVels...]

        info["timestamp"] = sim.timestamp
        info["tile_coords"] = [get_grid_coords(_road_tile_size(sim), pos)]
        # info['map_data'] = self.map_data
    end
    misc = Dict()
    misc["Simulator"] = info
    return misc
end

Zygote.@nograd get_agent_info

function compute_reward(sim::Simulator, pos, angle, speed)
    # Compute the collision avoidance penalty
    col_penalty = _proximity_penalty2(sim, pos, angle)
    # Get the position relative to the right lane tangent
    lp = get_lane_pos2(sim.fixedparams, pos, angle)

    reward = (
            speed * lp.dot_dir -
            10f0 * abs(lp.dist) +
            40f0 * col_penalty
    )
    return reward
end

function step!(sim::Simulator, action::Vector{Float32})
    # action = clamp.(action, -1f0, 1f0)

    for _ in 1:sim.fixedparams.frame_skip
        update_physics(sim, action)
    end

    # Generate the current camera image
    s = render_obs(sim)
    misc = get_agent_info(sim)

    d = _compute_done_reward(sim)
    sim.done = d.done
    misc["Simulator"]["msg"] = d.done_why
    return s, action, d.reward, sim.done, misc
end

function _compute_done_reward(sim::Simulator)
    # If the agent is not in a valid pose (on drivable tiles)
    if !(sim.fixedparams.train || _valid_pose(sim.fixedparams, sim.cur_pos, sim.cur_angle))
        msg = "Stopping the simulator because we are at an invalid pose."
        #logger.info(msg)
        reward = REWARD_INVALID_POSE
        done_code = "invalid-pose"
        done = true
    # If the maximum time step count is reached
    elseif sim.step_count ≥ _max_steps(sim)
        msg = "Stopping the simulator because we reached max_steps = $(_max_steps(sim))"
        #logger.info(msg)
        done = true
        reward = 0f0
        done_code = "max-steps-reached"
    else
        done = false
        reward = compute_reward(sim, sim.cur_pos, sim.cur_angle, sim.speed)
        msg = ""
        done_code = "in-progress"
    end
    return DoneRewardInfo(done, msg, reward, done_code)
end

function draw_ground_road(fp::FixedSimParams)
    # Draw the ground quad
    scene = Vector{Triangle}()
    trans_mat = scale_mat([50f0, 1f0, 50f0])
    ground_vlist = transform_mat(_grid(fp).ground_vlist, trans_mat)
    ground_scene = triangulate_faces(ground_vlist, nothing, fp.ground_color)
    scene =  vcat(scene, ground_scene)

    grid_width, grid_height = _grid(fp).grid_width, _grid(fp).grid_height

    # For each grid tile
    for j in 1:grid_height
        for i in 1:grid_width
            # Get the tile type and angle
            tile = _get_tile(_grid(fp), i, j)

            isnothing(tile) && continue

            # kind = tile["kind"]
            angle = tile["angle"]
            color = tile["color"]
            texture = (color_diffuse = color,
                       color_ambient = Vec3(1.0f0),
                       color_specular = Vec3(1.0f0),
                       specular_exponent = 50.0f0,
                       texture_ambient = nothing,
                       texture_diffuse = tile["texture"],
                       texture_specular = nothing)

            pos = [(i-0.5f0), 0f0, (j-0.5f0)] * _road_tile_size(fp)
            trans_mat = translation_mat(pos)
            trans_mat = rotate_mat(angle * 90f0) * trans_mat

            # Bind the appropriate texture
            road_vlist = _grid(fp).road_vlist
            road_tlist = _grid(fp).road_tlist
            road_vlist = transform_mat(road_vlist, trans_mat)
            scene = vcat(scene, triangulate_faces(road_vlist, road_tlist, color, texture))
            if tile["drivable"] && fp.draw_curve
                # Find curve with largest dotproduct with heading
                curves = _get_tile(_grid(fp), i, j)["curves"]
                curve_headings = permutedims(curves[end, :, :] .- curves[1, :, :])
                curve_headings = curve_headings / norm(curve_headings)
                dirVec = get_dir_vec(angle)
                dot_prods = map(i->dot(curve_headings[:, i], dirVec), 1:size(curve_heading, 2))
                dot_prods
                # Current (closest) curve drawn in Red
                pts = curves[:, :, argmax(dot_prods)]
                bezier_draw(pts, 20, true)

                pts = _get_curve(_grid(fp), i, j)
                for (idx, pt) in enumerate(pts)
                    # Don't draw current curve in blue
                    idx == argmax(dot_prods) && continue
                    bezier_draw(pt, 20)
                end
            end
        end
    end

    return scene
end

function _render_img(fp::FixedSimParams, cur_pos::Vector{Float32}, cur_angle::Float32, top_down::Bool=true)
    ##
    #Render an image of the environment into a frame buffer
    #Produce a numpy RGB array image as output
    ##

    !fp.graphics && return
    scene = Vector{Triangle}()

    pos, angle = cur_pos, cur_angle
    if fp.domain_rand
        pos = pos .+ fp.randomization_settings["camera_noise"]
    end

    x, y, z = pos .+ fp.cam_offset
    dx, dy, dz = get_dir_vec(angle)

    # Modelview matrix
    #mv_mat = Matrix{Float32}(I, 4, 4)

    if fp.draw_bbox
        y += 0.8f0
    elseif !top_down
        y += fp.cam_height
    end

    cam_width, cam_height = fp.camera_width, fp.camera_height
    grid_width, grid_height = _grid(fp).grid_width, _grid(fp).grid_height

    # Focal length for raytrace is 1m while that for rasterization is 20mm
    focus = fp.raytrace ? 1f0 : 20f0

    if top_down
        x = (grid_width * _road_tile_size(fp)) / 2f0
        y = 5f0
        z = (grid_height * _road_tile_size(fp)) / 2f0

        eye = Vec3([x], [y], [z])
        target = Vec3([x], [0f0], [z])
        vup = Vec3([0f0], [0f0], [-1f0])
        cam = Camera(eye, target, vup, fp.cam_fov_y, focus, cam_width, cam_height)
    else
        eye = Vec3([x], [y], [z])
        target = Vec3([x + dx], [y + dy], [z + dz])
        vup = Vec3([0f0], [1f0], [0f0])
        cam = Camera(eye, target, vup, fp.cam_fov_y, focus, cam_width, cam_height)
    end

    scene = vcat(scene, draw_ground_road(fp))

    # For each object
    objs = _objects(fp)
    if length(objs) > 0
        # Contains Vector{Vector{Δ}}
        obj_Δs=filter(o->!isnothing(o), map(obj->render(obj, fp.draw_bbox), objs))
        for oΔ in obj_Δs
            scene = vcat(scene, oΔ)
        end
    end

    # Draw the agent's own bounding box
    if fp.draw_bbox
        #corners = get_agent_corners(pos, angle)
        #gl.glColor3f(1, 0, 0)
        #gl.glBegin(gl.GL_LINE_LOOP)
        #gl.glVertex3f(corners[0, 0], 0.01, corners[0, 1])
        #gl.glVertex3f(corners[1, 0], 0.01, corners[1, 1])
        #gl.glVertex3f(corners[2, 0], 0.01, corners[2, 1])
        #gl.glVertex3f(corners[3, 0], 0.01, corners[3, 1])
        #gl.glEnd()
    end

    if top_down
        trans_mat = translation_mat(pos...)
        trans_mat = scale_mat(1f0) * trans_mat
        trans_mat = rotate_mat(rad2deg(sim.cur_angle)) * trans_mat
        # glColor3f(*self.color)
        scene = vcat(scene, render(sim.fixedparams.mesh))
        #gl.glPopMatrix()
    end

    #img_array = np.ascontiguousarray(np.flip(img_array, axis=0))

    return scene, cam
end

function viewable_scene(scene::Vector{Triangle}, pos, angle)
    dirVec = get_dir_vec(angle)
    dirVec = Vec3(dirVec[1:1], dirVec[2:2], dirVec[3:3])
    pos = Vec3(pos[1:1], pos[2:2], pos[3:3])
    new_scene = []
    for tri in scene
        # If angle between a vertex and bot is > π/2, it can't see that vertex.
        # angle > π/2 => cos is -ve
        # If all vertices of a triangle make an angle > π/2 with the bot,
        # that triangle is not visible to the bot.
        dot_prods = [RayTracer.dot(tri.v1 - pos, dirVec),
                     RayTracer.dot(tri.v2 - pos, dirVec),
                     RayTracer.dot(tri.v3 - pos, dirVec)]
        if !all(i -> all(i .< 0f0), dot_prods)
            new_scene = vcat(new_scene, [tri])
        end
    end
    return new_scene
end

function render_obs(sim::Simulator)
    ##
    #Render an observation from the point of view of the agent
    ##

    observation, cam = _render_img(
            sim.fixedparams,
            sim.cur_pos,
            sim.cur_angle,
	    false
    )
    # Take only the viewable triangles for rendering
    #observation = vcat(observation, viewable_scene(sim.scene, sim.cur_pos, sim.cur_angle))
    # self.undistort - for UndistortWrapper
    #NOTE: Not distorting as of now
    #if sim.distortion && !sim.undistort
    #    observation = distort(sim.camera_model, observation)
    #end

    # Setup some basic lighting with a far away sun
    if sim.fixedparams.raytrace
        if sim.fixedparams.domain_rand
            light_pos = sim.fixedparams.randomization_settings["light_pos"]
            light_pos = Float32.(light_pos)
            light_pos = Vec3(light_pos...)
        else
            light_pos = Vec3([-40f0], [200f0], [100f0])
        end

        ambient = _perturb(sim.fixedparams, Vec3([0.5f0]), 0.3f0)
        # XXX: diffuse is not used?
        diffuse = _perturb(sim.fixedparams, Vec3([0.7f0]), 0.3f0)
        #=
        from pyglet import gl
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_POSITION, (gl.GLfloat * 4)(*light_pos))
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_AMBIENT, (gl.GLfloat * 4)(*ambient))
        gl.glLightfv(gl.GL_LIGHT0, gl.GL_DIFFUSE, (gl.GLfloat * 4)(0.5, 0.5, 0.5, 1.0))
        gl.glEnable(gl.GL_LIGHT0)
        gl.glEnable(gl.GL_LIGHTING)
        gl.glEnable(gl.GL_COLOR_MATERIAL)
        =#

        #light = DistantLight(Vec3([1f0]), 5000f0, Vec3([0f0], [1f0], [0f0]))
        light = PointLight(Vec3([1f0]), 5f15, light_pos)
        origin, direction = get_primary_rays(cam)

        im = raytrace(origin, direction, observation, light, origin, 2)
    else
        #@warn "Rasterization may not work perfectly"
        im = rasterize(cam, observation)#, 0.04f0, 100f0)
    end

    color_r = reshape(im.x, sim.fixedparams.camera_width, sim.fixedparams.camera_height)
    color_g = reshape(im.y, sim.fixedparams.camera_width, sim.fixedparams.camera_height)
    color_b = reshape(im.z, sim.fixedparams.camera_width, sim.fixedparams.camera_height)

    shape = (sim.fixedparams.camera_width, sim.fixedparams.camera_height, 3, 1)
    im_arr = zeroonenorm(reshape(hcat(color_r, color_g, color_b), shape))
    return im_arr
end

function render(sim::Simulator, mode::String="human", close::Bool=false)
    ##
    #Render the environment for human viewing
    ##
    #=
    if close:
        if self.window:
            self.window.close()
        return

    top_down = mode == 'top_down'
    # Render the image
    img = self._render_img(
            WINDOW_WIDTH,
            WINDOW_HEIGHT,
            self.multi_fbo_human,
            self.final_fbo_human,
            self.img_array_human,
            top_down=top_down
    )

    # self.undistort - for UndistortWrapper
    if self.distortion and not self.undistort and mode != "free_cam":
        img = self.camera_model.distort(img)

    if mode == 'rgb_array':
        return img

    from pyglet import gl, window, image

    if self.window is None:
        config = gl.Config(double_buffer=False)
        self.window = window.Window(
                width=WINDOW_WIDTH,
                height=WINDOW_HEIGHT,
                resizable=False,
                config=config
        )

    self.window.clear()
    self.window.switch_to()
    self.window.dispatch_events()

    # Bind the default frame buffer
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)

    # Setup orghogonal projection
    gl.glMatrixMode(gl.GL_PROJECTION)
    gl.glLoadIdentity()
    gl.glMatrixMode(gl.GL_MODELVIEW)
    gl.glLoadIdentity()
    gl.glOrtho(0, WINDOW_WIDTH, 0, WINDOW_HEIGHT, 0, 10)

    # Draw the image to the rendering window
    width = img.shape[1]
    height = img.shape[0]
    img = np.ascontiguousarray(np.flip(img, axis=0))
    img_data = image.ImageData(
            width,
            height,
            'RGB',
            img.ctypes.data_as(POINTER(gl.GLubyte)),
            pitch=width * 3,
    )
    img_data.blit(
            0,
            0,
            0,
            width=WINDOW_WIDTH,
            height=WINDOW_HEIGHT
    )

    # Display position/state information
    if mode != "free_cam":
        x, y, z = self.cur_pos
        self.text_label.text = "pos: (%.2f, %.2f, %.2f), angle: %d, steps: %d, speed: %.2f m/s" % (
            x, y, z,
            int(self.cur_angle * 180 / math.pi),
            self.step_count,
            self.speed
        )
        self.text_label.draw()

    # Force execution of queued commands
    gl.glFlush()
=#
end

function _update_pos(pos, angle, wheel_dist, wheelVels, deltaTime)
    ##
    #Update the position of the robot, simulating differential drive
    #
    #returns new_pos, new_angle
    ##
    Vl, Vr = wheelVels
    l = wheel_dist

    # If the wheel velocities are the same, then there is no rotation
    if Vl == Vr
        pos = pos .+ deltaTime * Vl * get_dir_vec(angle)
        return pos, angle
    end

    # Compute the angular rotation velocity about the ICC (center of curvature)
    w = (Vr - Vl) / l

    # Compute the distance to the center of curvature
    r = (l * (Vl + Vr)) / (2f0(Vl - Vr))

    # Compute the rotation angle for this time step
    rotAngle = w * deltaTime

    # Rotate the robot's position around the center of rotation
    r_vec = get_right_vec(angle)
    px, py, pz = pos
    cx = px .+ r * r_vec[1]
    cz = pz .+ r * r_vec[3]
    npx, npz = rotate_point(px, pz, cx, cz, rotAngle)
    pos = [npx, py, npz]

    # Update the robot's direction angle
    angle = angle + rotAngle
    return pos, angle
end

function _actual_center(pos, angle)
    ##
    #Calculate the position of the geometric center of the agent
    #The value of self.cur_pos is the center of rotation.
    ##

    dir_vec = get_dir_vec(angle)
    return pos .+ (CAMERA_FORWARD_DIST - 0.5f0 * ROBOT_LENGTH) .* dir_vec
end

function get_agent_corners(pos, angle)
    agent_corners = agent_boundbox(
            _actual_center(pos, angle),
            ROBOT_WIDTH,
            ROBOT_LENGTH,
            get_dir_vec(angle),
            get_right_vec(angle)
    )
    return agent_corners
end


text:
using Base: PkgId

# A near-copy of `base/loading.jl`. However, this retains the full module path to the file.
function parse_cache_header(f::IO)
    modules = Vector{Pair{PkgId, UInt64}}()
    while true
        n = read(f, Int32)
        n == 0 && break
        sym = String(read(f, n)) # module name
        uuid = UUID((read(f, UInt64), read(f, UInt64))) # pkg UUID
        build_id = read(f, UInt64) # build UUID (mostly just a timestamp)
        push!(modules, PkgId(uuid, sym) => build_id)
    end
    totbytes = read(f, Int64) # total bytes for file dependencies
    # read the list of requirements
    # and split the list into include and requires statements
    includes = Tuple{Module, String, Float64}[]
    requires = Pair{Module, PkgId}[]
    while true
        n2 = read(f, Int32)
        n2 == 0 && break
        depname = String(read(f, n2))
        mtime = read(f, Float64)
        n1 = read(f, Int32)
        mod = (n1 == 0) ? Main : Base.root_module(modules[n1].first)
        if n1 != 0
            # determine the complete module path
            while true
                n1 = read(f, Int32)
                totbytes -= 4
                n1 == 0 && break
                submodname = String(read(f, n1))
                mod = getfield(mod, Symbol(submodname))
                totbytes -= n1
            end
        end
        if depname[1] != '\0'
            push!(includes, (mod, depname, mtime))
        end
        totbytes -= 4 + 4 + n2 + 8
    end
    @assert totbytes == 12 "header of cache file appears to be corrupt"
    return modules, (includes, requires)
end

function parse_cache_header(cachefile::String)
    io = open(cachefile, "r")
    try
        !Base.isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile."))
        return parse_cache_header(io)
    finally
        close(io)
    end
end

"""
    parse_pkg_files(modsym)

This function gets called by `watch_package` and runs when a package is first loaded.
Its job is to organize the files and expressions defining the module so that later we can
detect and process revisions.
"""
function parse_pkg_files(id::PkgId)
    files = String[]
    modsym = Symbol(id.name)
    if use_compiled_modules()
        # We probably got the top-level file from the precompile cache
        # Try to find the matching cache file
        uuid = id.uuid
        paths = Base.find_all_in_cache_path(id)
        for path in paths
            provides, includes_requires = parse_cache_header(path)
            mods_files_mtimes, _ = includes_requires
            for (pkgid, buildid) in provides
                if pkgid.uuid === uuid && pkgid.name == id.name
                    # found the right cache file
                    for (mod, fname, _) in mods_files_mtimes
                        # For precompiled packages, we can read the source later (whenever we need it)
                        # from the *.ji cachefile.
                        fileinfos[fname] = FileInfo(mod, path)
                        push!(files, fname)
                        module2files[modsym] = files
                    end
                    return files
                end
            end
        end
    end
    # Non-precompiled package(s). Here we rely on the `include` callbacks to have
    # already populated `included_files`; all we have to do is collect the relevant
    # files.
    queue_includes!(files, id.name)
    module2files[modsym] = files
    files
end

# The main trick here is that since `using` is recursive, `included_files`
# might contain files associated with many different packages. We have to figure
# out which correspond to a particular module `mod`, which we do by:
#   - checking the module in which each file is evaluated. This suffices to
#     detect "supporting" files, i.e., those `included` within the module
#     definition.
#   - checking the filename. Since the "top level" file is evaluated into Main,
#     we can't use the module-of-evaluation to find it. Here we hope that the
#     top-level filename follows convention and matches the module. TODO?: it's
#     possible that this needs to be supplemented with parsing.
function queue_includes!(files, modstring)
    delids = Int[]
    for i = 1:length(included_files)
        mod, fname = included_files[i]
        modname = String(Symbol(mod))
        if startswith(modname, modstring) || endswith(fname, modstring*".jl")
            fm = parse_source(fname, mod)
            instantiate_sigs!(fm)
            if fm != nothing
                fileinfos[fname] = FileInfo(fm)
            end
            push!(files, fname)
            push!(delids, i)
        end
    end
    deleteat!(included_files, delids)
    return files
end

function queue_includes(mod::Module)
    files = queue_includes!(String[], String(Symbol(mod)))
    init_watching(files)
end

# A near-duplicate of some of the functionality of queue_includes!
# This gets called for silenced packages, to make sure they don't "contaminate"
# included_files
function remove_from_included_files(modsym::Symbol)
    i = 1
    modstring = string(modsym)
    while i <= length(included_files)
        mod, fname = included_files[i]
        modname = String(Symbol(mod))
        if startswith(modname, modstring) || endswith(fname, modstring*".jl")
            deleteat!(included_files, i)
        else
            i += 1
        end
    end
end

function read_from_cache(fi::FileInfo, file::AbstractString)
    if fi.cachefile == basesrccache
        # Get the original path
        filec = get(cache_file_key, file, file)
        return open(basesrccache) do io
            Base._read_dependency_src(io, filec)
        end
    end
    Base.read_dependency_src(fi.cachefile, file)
end

function maybe_parse_from_cache!(fi::FileInfo, file::AbstractString)
    if isempty(fi.fm)
        # Source was never parsed, get it from the precompile cache
        src = read_from_cache(fi, file)
        topmod = first(keys(fi.fm))
        if parse_source!(fi.fm, src, Symbol(file), 1, topmod) === nothing
            @error "failed to parse cache file source text for $file"
        end
        instantiate_sigs!(fi.fm)
    end
    return fi
end

function watch_files_via_dir(dirname)
    wait_changed(dirname)  # this will block until there is a modification
    latestfiles = String[]
    wf = watched_files[dirname]
    for file in wf.trackedfiles
        path = joinpath(dirname, file)
        if mtime(path) + 1 >= floor(wf.timestamp) # OSX rounds mtime up, see #22
            push!(latestfiles, path)
        end
    end
    updatetime!(wf)
    latestfiles
end

"""
    watch_package(id::Base.PkgId)

Start watching a package for changes to the files that define it.
This function gets called via a callback registered with `Base.require`, at the completion
of module-loading by `using` or `import`.
"""
function watch_package(id::PkgId)
    # Because the callbacks are made with `invokelatest`, for reasons of performance
    # we need to make sure this function is fast to compile. By hiding the real
    # work behind a @async, we truncate the chain of dependency.
    @async _watch_package(id)
end

@noinline function _watch_package(id::PkgId)
    modsym = Symbol(id.name)
    if modsym ∈ dont_watch_pkgs
        if modsym ∉ silence_pkgs
            @warn "$modsym is excluded from watching by Revise. Use Revise.silence(\"$modsym\") to quiet this warning."
        end
        remove_from_included_files(modsym)
        return nothing
    end
    files = parse_pkg_files(id)
    init_watching(files)
    nothing
end

ext: jl


text:
# copied from CUDAnative PR 422

# Gets a pointer to a global with a particular name. If the global
# does not exist yet, then it is declared in the global memory address
# space.
@generated function get_global_pointer(::Val{global_name}, ::Type{T})::AMDGPUnative.DevicePtr{T} where {global_name, T}
    T_global = convert(LLVMType, T)
    T_result = convert(LLVMType, Ptr{T})

    # Create a thunk that computes a pointer to the global.
    llvm_f, _ = create_function(T_result)
    mod = LLVM.parent(llvm_f)

    # Figure out if the global has been defined already.
    global_set = LLVM.globals(mod)
    global_name_string = String(global_name)
    if haskey(global_set, global_name_string)
        global_var = global_set[global_name_string]
    else
        # If the global hasn't been defined already, then we'll define
        # it in the global address space, i.e., address space one.
        global_var = GlobalVariable(mod, T_global, global_name_string, 1)
        linkage!(global_var, LLVM.API.LLVMExternalLinkage)
        extinit!(global_var, true)
        set_used!(mod, global_var)
    end

    # Generate IR that computes the global's address.
    Builder(JuliaContext()) do builder
        entry = BasicBlock(llvm_f, "entry", JuliaContext())
        position!(builder, entry)

        # Cast the global variable's type to the result type.
        result = ptrtoint!(builder, global_var, T_result)
        ret!(builder, result)
    end

    # Call the function.
    quote
        AMDGPUnative.DevicePtr{T, AMDGPUnative.AS.Global}(convert(Csize_t, $(call_function(llvm_f, Ptr{T}))))
    end
end


text:
bouts = preprocess_bouts(pokes)
open_html_table(bouts[1:1000,:])


text:
module μodule

export @μ

macro μ(words::Symbol...)
    token = :μ
    for word in words
        token = μagic(word, token)
    end
    return esc(token)
end

function μagic(word::Symbol, token)
    glyphs = [Symbol(glyph) for glyph in string(word)]
    while length(glyphs) > 0
        glyph = eval(pop!(glyphs))
        token = glyph(glyphs, token)
    end
    return token
end

Table = Dict([
    :a => :+
    :e => :^
    :i => :*
    :o => :/
    :q => :\
    :u => :-
])

a(glyphs, token) = token
b(glyphs, token) = :(x = $token; (x, x))
d(glyphs, token) = (length(glyphs) > 0 ? (f = Table[pop!(glyphs)]; :(reduce($f, $token))) :
                    Expr(token.head, :reduce, token.args[2:end]...))
e(glyphs, token) = (:(x = $token; ($(eval(Expr(:call, pop!(glyphs), glyphs, :(:x)))), $(eval(Expr(:call, pop!(glyphs), glyphs, :(:x)))))))
f(glyphs, token) = (fname = Symbol(glyphs...); empty!(glyphs); :($fname(μ) = $token))
g(glyphs, token) = (fname = Symbol(glyphs...); empty!(glyphs); :($fname(μ...) = $token))
h(glyphs, token) = (:(first($token)))

k(glyphs, token) = :(collect($token))
l(glyphs, token) = :(last($token))

m(glyphs, token) = (f = Table[pop!(glyphs)]; :(map($f, $token)))
n(glyphs, token) = :(x = $token; (one(x), x))
p(glyphs, token) = :(prod($token))
q(glyphs, token) = Expr(token.head, reverse(token.args))
r(glyphs, token) = (reverse!(glyphs); token)
s(glyphs, token) = :(sum($token))
t(glyphs, token) = (:($token[2:end]))

u(glyphs, token) = :(x = $token; UnitRange(x...))
w(glyphs, token) = (n = Int(first("$(pop!(glyphs))")); :($token, $n))
!(glyphs::Vector{Symbol}, token) = (:())

end


text:
using StatisticalRethinking, Optim
#gr(size=(600,600));

p_grid = range(0, step=0.001, stop=1);

prior = ones(length(p_grid));

likelihood = [pdf(Binomial(9, p), 6) for p in p_grid];

posterior = likelihood .* prior;

posterior = posterior / sum(posterior);

N = 10000
samples = sample(p_grid, Weights(posterior), N);

chn = MCMCChains.Chains(reshape(samples, N, 1, 1), ["toss"]);

MCMCChains.describe(chn)

plot(chn)

x0 = [0.5]
lower = [0.0]
upper = [1.0]

function loglik(x)
  ll = 0.0
  ll += log.(pdf.(Beta(1, 1), x[1]))
  ll += sum(log.(pdf.(Binomial(9, x[1]), repeat([6], 1))))
  -ll
end

(qmap, opt) = quap(loglik, x0, lower, upper)

opt

quapfit = [qmap[1], std(samples, mean=qmap[1])]

p = Vector{Plots.Plot{Plots.GRBackend}}(undef, 4)
p[1] = scatter(1:length(p_grid), samples, markersize = 2, ylim=(0.0, 1.3), lab="Draws")

w = 6
n = 9
x = 0:0.01:1
p[2] = plot( x, pdf.(Beta( w+1 , n-w+1 ) , x ), lab="Conjugate solution")
density!(p[2], samples, lab="Sample density")

p[3] = plot( x, pdf.(Beta( w+1 , n-w+1 ) , x ), lab="Conjugate solution")
plot!( p[3], x, pdf.(Normal( quapfit[1], quapfit[2] ) , x ), lab="Quap approximation")

w = 6; n = 9; x = 0:0.01:1
p[4] = plot( x, pdf.(Beta( w+1 , n-w+1 ) , x ), lab="Conjugate solution")
plot!(p[4], x, pdf.(Normal( 0.67 , 0.16 ) , x ), lab="Normal approximation")
plot(p..., layout=(2, 2))

# This file was generated using Literate.jl, https://github.com/fredrikekre/Literate.jl


text:
# This file was generated, do not modify it. # hide
using PyPlot
figure()
plot([0, 1], [0, 1])
savefig(joinpath(@OUTPUT, "test.png"))

max_stars_repo_name: mcognetta/franklindocs

max_issues_repo_name: mcognetta/franklindocs

max_forks_repo_name: mcognetta/franklindocs


text:
include("test_Plot1.jl")
include("test_Plot2.jl")
include("test_Plot3.jl")
include("test_Plot4.jl")
include("test_Plot5.jl")
include("test_Plot6.jl")
include("test_Plot7.jl")


text:
import Dates:
    Date,
    DateTime,
    UInt64

import Base:
    collect,
    convert,
    copy,
    eltype,
    float,
    isempty,
    iterate,
    lastindex,
    length,
    ndims,
    show,
    size

# outermost allowed element type
const VALTYPE = Union{Float64,Int64,Bool}
const IDXTYPE = Union{Date,DateTime, UInt64}
const FLDTYPE = Union{Symbol,String,Char}
const VALARR = AbstractArray{<:VALTYPE}
const IDXARR = AbstractVector{<:IDXTYPE}
const FLDARR = AbstractVector{<:FLDTYPE}

# type definition/constructor
@doc """
Time series type aimed at efficiency and simplicity.

_Fields_:

    - `values`: 2-dimensional array of numerical data
    - `index`: 1-dimensional array of `Date`,`DateTime` or `UInt64` representing index where element t corresponds t'th row of `values`
    - `fields`: 1-dimensional array of `Symbol` representing columns where element j corresponds to j'th column of `values`
""" ->
mutable struct TS{V<:VALTYPE,T<:IDXTYPE}
    values::Matrix{V}
    index::Vector{T}
    fields::Vector{Symbol}
    function TS(values::Matrix{V}, index::Vector{T}, fields::Vector{Symbol}) where {V<:VALTYPE, T<:IDXTYPE}
        @assert size(values,1)==length(index) "Length of index not equal to number of value rows."
        @assert size(values,2)==length(fields) "Length of fields not equal to number of columns in values."
        order = sortperm(index)
        return new{V,T}(values[order,:], index[order], fields)
    end
end

# alias
const ts = TS

# basic utilities
collect(x::TS) = x
copy(x::TS) = TS(copy(x.values), copy(x.index), copy(x.fields))
eltype(x::TS) = eltype(x.values)
first(x::TS) = x[1]
isempty(x::TS) = (isempty(x.index) && isempty(x.values))
iterate(x::TS) = size(x,1) == 0 ? nothing : (x.index[1], x.values[1,:]), 2
iterate(x::TS, i::Int) = i == lastindex(x, 1) + 1 ? nothing : ((x.index[i], x.values[i,:]), i+1)
last(x::TS) = x[end]
lastindex(x::TS) = lastindex(x.values)
lastindex(x::TS, d) = lastindex(x.values, d)
length(x::TS) = prod(size(x))::Int
ndims(::TS) = 2
ndims(::Type{TS{V,T}}) where {V<:VALTYPE,T<:IDXTYPE} = 2
size(x::TS) = size(x.values)
size(x::TS, dim::Int) = size(x.values, dim)


text:
module CompositeConvex

export pNorm,
       pqNorm,
       SchattenNorm,
       compute_norm,
       dual_norm,
       compute_norm_projection!,
       affine_model!,
       ProblemParams,
       SubproblemOptParams,
       min_composite

include("Norms.jl")
import .Norms: pNorm, pqNorm, SchattenNorm, compute_norm, dual_norm, compute_norm_projection!

include("Utils.jl")
import .Utils: affine_model!

include("MinComposite.jl")
import .MinComposite: ProblemParams, SubproblemOptParams, min_composite

end


text:
struct TestQss <: QuerySynthesisStrategy
    optimizer::QuerySynthesisOptimizer
    TestQss(; optimizer::QuerySynthesisOptimizer) = new(optimizer)
end

function qs_score_function(qs::TestQss, x::Array{T, 2}, labels::Dict{Symbol, Array{Int, 1}})::Function where T <: Real
    return x -> -abs.(sum(x, dims=1))
end


text:
#using LinearAlgebra, Plots, ForwardDiff, DataFrames
#pyplot(framestyle = :box, color = :black, linestyle = :auto)

# Assignment 1

## Descent Functions for gradient-based optimization

### Steepest Descent
function descent_steepest(f::Function, position_current::Vector)
    # Determine the gradient at selected position
    grad_current = ForwardDiff.gradient(f, position_current)

    # Steepest descent
    d = - grad_current

    # Output descent direction (vector), gradient at current step (Vector)
    return d
end

### Conjugate gradient
function descent_conjugate(grad_current::Vector, grad_prev::Vector, d_prev::Vector)
    #Determine the value of β_k for the Conjugate Gradient search method
    β = (norm(grad_current) / norm(grad_prev))^2

    #Determine new direction vector for current step
    d = -grad_current .+ β * d_prev

    # Output descent direction (Vector), gradient at current step (Vector)
    return d
end

### Newton
function descent_newton(grad_current::Vector, hess_current::Array, position_current::Vector; det_err = 1e-8)
    #Two conditions must be met for newton method to be valid.

    #Condition 1: Positive Semi-Definite Hessian
    #IE if there is an eigenvalue of the Hessian that is less than 0, the Hessian is not positive semi-definite
    #and the newton method cannot be used
    if any(eigvals(hess_current) .< 0)
        println("Hessian of function at this point is not positive semi-definite. Newton method cannot be used.")
        return
    end

    #Condition 2: Hessian must be non-singular
    #IE: The Hessian must be invertible,
    #IE2: The determinant of the Hessian must be non-zero
    #Note that det_err provides a safeguard against numerical errors when checking for the determinant value
    if abs(det(hess_current)) < det_err
        println("Hessian of function at this point is singular. Newton method cannot be used.")
        return
    end

    #If both requirements pass, then the newton method provides the output:
    d = -inv(hess_current) * grad_current

    return d
end

# Here's another git test

## Step Function (Line search)
function α_step(func::Function, position_current::Vector, d_current::Vector, α_bar; ϵ = 1e-4, intervals = 50, max_iter = 100)
    #α_bar is the upper range for α, set as 1.0 typically, IE 100% of the gradient

    #Initiate values
    err = 1
    lower = 0 #Lower bound is 0, giving f(x_(k+1)) = f(x_k)
    upper = α_bar #Upper bound is the user defined α_bar
    iter = 0

    while err > ϵ #While the error remains large

        #Update iteration count
        iter += 1

        if iter > max_iter
            println("Iteration time out for search. Increase maximum iteration number or revise input.")
            return
        end

        #Range of increasing values of α from 0 to α_bar
        α_range = collect(range(lower, length = intervals, stop = upper))
        #This creates an array of length = 'intervals' from lower bound to upper bound

        #f_test_range is the value of f(x) for each increment α_i ∈ α_range
        f_test_range = [func(position_current .+ α_range[i] .* d_current) for i = 1:intervals]

        #Find the incremental difference in value of f_test_range
        f_diff = diff(f_test_range)

        #Note that it is possible that the selected range has a continuously decreasing function, in which case
        #the optimum value of α is not determined in the selection range, therefore incerase the upper bound on α
        if all(f_diff .< 0)
            upper = 2 * upper
            continue
        end

        #find the index of the first instance where the function begins to increase
        idx_lower = findfirst(x-> x>0, f_diff) - 1 #lower bound of new interval
        idx_upper = idx_lower + 2 #upper bound of new interval

        #Updated lower and upper bounds for the optimal step size α
        lower = α_range[idx_lower]
        upper = α_range[idx_upper]

        #Calculate error
        err = upper - lower

    end

    #After this loop, the interval for the upper and lower limits for α has been determined
    #Return the average between the bounds
    α_opt = (upper + lower) / 2

    #return the optimal step size + number of iterations taken to reach it
    return α_opt, iter

end

## Main Unconstrainted Optimization function
#Define the error calculating function
function gradnorm(func::Function, x::Vector)
    return norm(ForwardDiff.gradient(func, x))
end

function minimize_unconstrained(func::Function,
        x_start::Array,
        descent_method,
        iter_max::Int,
        α_max::Float64,
        ϵ::Float64;
        annotation = true)

    #Initiate storage vectors for relevant data:
    iter_store = [] #number of iterations
    x_store = [] #value of design variables at each iteration
    f_store = [] #value of function at each iteration
    grad_store = [] #gradient at each iteration
    err_store = [] #error value at each iteration (norm of gradient)
    iter_linesearch_store = [] #number of iterations taken for line search algorithm
    α_store = [] #step size
    d_store = [] #descent vector

    #initiate values value
    err = 1
    x = x_start
    iter = 0

    while err > ϵ && iter < iter_max
        iter += 1

        #Iteration values
        f = func(x)
        grad = ForwardDiff.gradient(func,x)
        hess = ForwardDiff.hessian(func,x)
        err = gradnorm(func, x)

        #Find descent direciton, d

        if descent_method == :steepest
            d = -grad
        #NOTE:Since the conjugate gradient method relies on the gradient and direction vector
        # of the previous step, the direction vector for the FIRST step is set to the steepest descent method
        elseif descent_method == :cg && iter == 1
            d = -grad
        elseif descent_method == :cg
            d = descent_conjugate(grad, grad_store[iter-1], d_store[iter-1])
        elseif descent_method == :newton
            d = descent_newton(grad, hess, x)
        else
            println("Descent method unrecognized.")
            println("Allowable methods: :steepest, :cg, :newton")
            return
        end

        #find the step size, α, and the number of iterations to solve for it, α_iter
        α, α_iter = α_step(func, x, d, α_max)

        #Store all relevant values
        push!(iter_store, iter -1) #number of main loop iterations
        push!(x_store, x) #value of design variables
        push!(f_store, f) #value of function
        push!(grad_store, grad) #value of gradient
        push!(err_store, err) #value of error (norm of gradient)
        push!(α_store, α) #value of step size
        push!(iter_linesearch_store, α_iter) #number of line search iterations
        push!(d_store, d) #step direction vector

        #update the value of the design variables,
        x = x .+ α .* d

        if annotation
            println("Iteration: ", iter-1)
            println("Function value: ", round(f, digits = 2))
            println("Error (Norm(Gradient)): ", round(err, digits = 2))
            println("Line search iterations: ", α_iter)
            println("Step size: ", round(α, digits = 2))
            println("________________________")
        end
    end

    if iter == iter_max
        println("Iteration time out. Revise.")
        return
    end

    #Output all necessary information as a DataFrame type for easy export + plotting
    output = DataFrame(N_iteration = iter_store,
        x_val = x_store,
        Function_val = f_store,
        Gradient = grad_store,
        Gradient_norm = err_store,
        Direction_vec = d_store,
        Step_size = α_store,
        Linesearch_iterations = iter_linesearch_store)

    return output
end

##Here's a secondary test to check git connection to github.

## A third test


text:
module GenVariableElimination

include("factor_graph.jl")
include("compiler.jl")
include("gen_fns.jl")

export FactorGraph
export variable_elimination, VariableEliminationResult
export Latent, Observation, compile_trace_to_factor_graph
export draw_factor_graph
export sample_factor_graph, compile_and_sample_factor_graph

end # module


text:
@testset "StateVector" begin
    dim = DIM
    T = ComplexF64

    vacuum_state_v = zeros(T, dim)
    vacuum_state_v[0 + 1] = 1

    state = StateVector{T}(vacuum_state_v, dim)
    @test vec(state) == vacuum_state_v
    @test state.dim == dim
    @test 𝛒(state) == vacuum_state_v * vacuum_state_v'
    @test purity(state) ≈ 1

    @test repr(VacuumState(dim=5)) == "StateVector{ComplexF64}(dim=5, vec=[\n" *
        " 1.0 + 0.0im\n 0.0 + 0.0im\n 0.0 + 0.0im\n 0.0 + 0.0im\n 0.0 + 0.0im\n])"

    s1 = SqueezedState(ξ(1.5, π/4), dim=5)
    s2 = SqueezedState(ξ(0.5, π/8), dim=10)
    v_ans = copy(s2.v); v_ans[1:5] .+= s1.v

    @test (s1 + s2).dim == 10
    @test (s1 + s2).v == v_ans
    @test (s2 + s1).dim == 10
    @test (s2 + s1).v == v_ans
    @test (0.5 * s1).v == 0.5 * s1.v
    @test (s2 * 0.5).v == 0.5 * s2.v

    # new_state = create!(copy(state))
    # @test new_state == SinglePhotonState(dim=dim)
    # @test state == VacuumState(dim=dim)
end

@testset "StateMatrix" begin
    dim = DIM
    T = ComplexF64

    vacuum_state_𝛒 = zeros(T, dim, dim)
    vacuum_state_𝛒[0+1, 0+1] = 1

    state = StateMatrix{T}(vacuum_state_𝛒, dim)
    @test state.dim == dim
    @test 𝛒(state) == vacuum_state_𝛒
    @test purity(state) ≈ 1

    @test repr(VacuumState(dim=5, rep=StateMatrix)) == "StateMatrix{ComplexF64}(dim=5, 𝛒=[\n" *
        " 1.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im\n" *
        " 0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im\n" *
        " 0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im\n" *
        " 0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im\n" *
        " 0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im  0.0 + 0.0im\n])"

    # new_state = create!(copy(state))
    # @test new_state == SinglePhotonState(rep=StateMatrix, dim=dim)
    # @test state == VacuumState(rep=StateMatrix, dim=dim)

    # constructor for state vector
    vacuum_state_v = zeros(T, dim)
    vacuum_state_v[0 + 1] = 1
    state_vector = StateVector{T}(vacuum_state_v, dim)
    @test state == StateMatrix(state_vector)

    s1 = SqueezedState(ξ(1.5, π/4), dim=5, rep=StateMatrix)
    s2 = SqueezedState(ξ(0.5, π/8), dim=10, rep=StateMatrix)
    𝛒_ans = copy(s2.𝛒); 𝛒_ans[1:5, 1:5] .+= s1.𝛒

    @test (s1 + s2).dim == 10
    @test (s1 + s2).𝛒 == 𝛒_ans
    @test (s2 + s1).dim == 10
    @test (s2 + s1).𝛒 == 𝛒_ans
    @test (0.5 * s1).𝛒 == 0.5 * s1.𝛒
    @test (s2 * 0.5).𝛒 == 0.5 * s2.𝛒
end


text:
# A list of functions

# Overloading an input function so anything can be put into it and it just
# returns the input
function f_input(x::Any, y::Any, c::Any)
    x
end

# For output Nodes, if input is a number and anything else, return the number
function f_output(x::Float64, y::Any, c::Any)
    x
end

# For output Nodes, if input is an array and anything else, return the mean of
# the array
function f_output(x::Array{Float64}, y::Any, c::Any)
    mean(x)
end

# Scale all values to the proper input range
function scaled(x::Float64)
    if isnan(x)
        return 0.0
    end
    min(max(x, -1.0), 1.0)
end

# Scale all values to the proper input range
function scaled(x::Array{Float64})
    x[isnan.(x)] = 0.0
    min.(max.(x, -1.0), 1.0)
end

# Not super sure what's going on here
function func2f(f::Function)
    findfirst(CGP.Config.functions .== f) / length(CGP.Config.functions)
end

# Function to find function index
function f2ind(list::Tuple, i::Float64)
    l = length(list)
    min(max(Int64(ceil(i*l)), 1), l)
end

# Function to find function index
function f2ind(list::Tuple, i::Array{Float64})
    l = length(list)
    min.(max.(Int64.(ceil.(i.*l)), 1), l)
end

# Function to find function index
function f2ind(list::Array, i::Float64)
    l = length(list)
    min(max(Int64(ceil(i*l)), 1), l)
end

# Function to find function index
function f2ind(list::Array, i::Array{Float64})
    l = length(list)
    min.(max.(Int64.(ceil.(i.*l)), 1), l)
end

# Converts list of functions to indices (?)
function index_in(list::Array, index::Float64)
    list[f2ind(list, abs(index))]
end

# Converts list of functions to indices (?)
function index_in(list::Array, index::Array{Float64})
    index_in(list, mean(abs.(index)))
end

# Converts list of functions to indices (?)
function index_in(list::Tuple, index::Float64)
    list[f2ind(list, abs(index))]
end

# Converts list of functions to indices (?)
function index_in(list::Tuple, index::Array{Float64})
    index_in(list, mean(abs.(index)))
end

#TODO: n-dimensional square indexing

# A segmentation function ?
function segmentation(x::Array{Float64}, p::Float64, f::Function)
    if ndims(x) == 2
        segments = f(x, p)
        return segments.image_indexmap / maximum(segments.segment_labels)
    else
        return x
    end
end

# Gets a range?
function range_in(list::Array{Float64}, xi::Float64, yi::Float64)
    # TODO: multi-dimensional
    bounds = [min(xi, yi), max(xi, yi)]
    bounds = f2ind(list, abs.(bounds))
    if bounds[1] == bounds[2]
        return 0.0
    end
    list[bounds[1]:bounds[2]]
end

# Gets a range?
function range_in(list::Array{Float64}, xi::Array{Float64}, yi::Float64)
    range_in(list, mean(xi), yi)
end

# Scaling indices
function scaled_indmax(x::Array{Float64})
    scaled(collect((ind2sub(x, indmax(x)) .- 1) ./ (size(x) .- 1)))
end

# Scaling indices
scaled_indmax(x::Array{Float64}, y::Array{Float64}) = scaled_indmax(x)

# I have no clue what's happening here
function com(x::Array{Float64})
    # TODO: speed up
    com = Tuple(zeros(ndims(x)))
    sx = sum(x.+1)
    if sx > 0
        for i in eachindex(x)
            com = com .+ ((x[i]+1) .* (ind2sub(x, i) .- 1 .- ((size(x) .- 1)./2)))
        end
        com = com ./ sx
    end
    scaled(collect((com .+ (size(x) .- 1) ./ 2) ./ (size(x) .- 1)))
end

# Gets the minimum sized array
function minsize(x::Array{Float64}, y::Array{Float64})
    dims = min(ndims(x), ndims(y))
    shape = [min(size(x, i), size(y, i)) for i in 1:dims]
    (x[[1:i for i in shape]...], y[[1:i for i in shape]...])
end

# Fills a smaller array with 1s
function fillsize(x::Array{Float64}, y::Array{Float64}, c::Float64)
    if ndims(x) == ndims(y)
        return paddedviews(c, x, y)
    elseif ndims(x) > ndims(y)
        mindim = ndims(y)+1
        return paddedviews(c, x, repeat(y, inner=tuple(
            ones(Int64, ndims(y))..., size(x)[mindim:end]...)))
    else
        mindim = ndims(x)+1
        return paddedviews(c, repeat(x, inner=tuple(
            ones(Int64, ndims(x))..., size(y)[mindim:end]...)), y)
    end
end

# Checks whether two arrays are equally sized
function eqsize(x::Array{Float64}, y::Array{Float64}, c::Float64)
    minsize(x, y)
end

# Generates funtions from strings
function sgen(name::String, s1::String, s2::String, s3::String, s4::String)
    eval(parse(string(name,
                      "(x::Float64, y::Float64, c::Float64=0.0)=",
                      s1, ";", name,
                      "(x::Float64, y::Array{Float64}, c::Float64=0.0)=",
                      s2, ";", name,
                      "(x::Array{Float64}, y::Float64, c::Float64=0.0)=",
                      s3, ";", name,
                      "(x::Array{Float64}, y::Array{Float64}, c::Float64=0.0)=",
                      s4)))
end

# Loads all the functions from a dictionary of functions
function load_functions(funs::Dict)
    newfuns = []
    for k in keys(funs)
        if isdefined(Config, parse(k))
            debug("Loading functions: $k is already defined, skipping")
        else
            if length(funs[k])==1
                sgen(k, funs[k][1], funs[k][1], funs[k][1], funs[k][1])
            elseif length(funs[k])==2
                sgen(k, funs[k][1], funs[k][1], funs[k][2], funs[k][2])
            else
                sgen(k, funs[k][1], funs[k][2], funs[k][3], funs[k][4])
            end
            append!(newfuns, [k])
        end
    end
    [eval(parse(k)) for k in newfuns]
end


text:
@testset "Box" begin
    b = Box([0,0], [2,4])  # specify center and radii
    @test b == deepcopy(b)
    @test hash(b) == hash(deepcopy(b))
    @test [0.3,-1.5] ∈ b
    @test [0.3,-2.5] ∉ b

    @test ((x,nout) = surfpt_nearby([0,0],b); (x≈[1,0] && nout≈[1,0]) || (x≈[0,2] && nout≈[0,1]))  # handle point at center properly
    @test all([(p = [1sx,2sy]; surfpt_nearby(1.1p,b) == (p, normalize(1.1p-p))) for sx = (-1,1), sy = (-1,1)])  # outside corners
    @test all([((x, nout) = surfpt_nearby([1sx,2sy],b); x≈[1sx,2sy] && all([sx 0; 0 sy]*nout.≥0) && norm(nout)≈1) for sx = (-1,1), sy = (-1,1)])  # at corners
    @test all([surfpt_nearby([ρ*1sx,1sy],b) == ([1sx,1sy], [sx,0]) for ρ = (one⁻⁻,1,one⁺⁺), sx = (-1,1), sy = (-1,0,1)])  # around faces
    @test all([surfpt_nearby([0.5sx,ρ*2sy],b) == ([0.5sx,2sy], [0,sy]) for ρ = (one⁻⁻,1,one⁺⁺), sx = (-1,0,1), sy = (-1,1)])  # around faces

    @test normal([1.1,0],b) == [1,0]
    @test normal([-1.1,0],b) == [-1,0]
    @test normal([1.1,2.1],b) == [1,1]/√2
    @test bounds(b) == ([-1,-2],[1,2])
    @test bounds(Box([0,0], [2,4], [1 1; 1 -1])) ≈ ([-3*√0.5,-3*√0.5], [3*√0.5,3*√0.5])
    @test checkbounds(b)
    @test checkbounds(Box([0,0], [2,4], [1 1; 1 -1]))

    @test (∆ = rand(2); translate(b,∆) ≈ Box([0,0]+∆, [2,4]))
end  # @testset "Box"

@testset "Box, rotated" begin
    ax1, ax2 = normalize.(([1,-1], [1,1]))
    r1, r2 = 1, 2  # "radii"
    br = Box([0,0], [2r1, 2r2], [ax1 ax2])

    R = [ax1 ax2]  # rotation matrix

    Cin = R * (GeometryPrimitives.signmatrix(br) .* (one⁻ .* [r1,r2]))  # around corners, inside
    Cout = R * (GeometryPrimitives.signmatrix(br) .* (one⁺ .* [r1,r2]))  # around corners, outside
    for j = 1:2; @test Cin[:,j] ∈ br; end
    for j = 1:2; @test Cout[:,j] ∉ br; end

    @test br == deepcopy(br)
    @test hash(br) == hash(deepcopy(br))

    @test ((x,nout) = surfpt_nearby([0,0],br); (x≈r1*ax1 && nout≈ax1) || (x≈r2*ax2 && nout≈ax2))  # handle point at center properly
    @test all([(p = (s1*r1*ax1+s2*r2*ax2); surfpt_nearby(1.1p,br) ≈ (p,normalize(1.1p-p))) for s1 = (-1,1), s2 = (-1,1)])  # outside corners
    @test all([(p = (s1*r1*ax1+s2*r2*ax2); (x,nout) = surfpt_nearby(p,br); x≈p && all([s1*ax1 s2*ax2]'*nout.≥0) && norm(nout)≈1) for s1 = (-1,1), s2 = (-1,1)])  # at corners
    @test all([(p1 = s1*r1*ax1; p2 = s2*r2/2*ax2; surfpt_nearby(ρ*p1+p2,br) ≈ (p1+p2, s1*ax1)) for ρ = (one⁻⁻,1,one⁺⁺), s1 = (-1,1), s2 = (-1,0,1)])  # around faces
    @test all([(p1 = s1*r1/2*ax1; p2 = s2*r2*ax2; surfpt_nearby(p1+ρ*p2,br) ≈ (p1+p2, s2*ax2)) for ρ = (one⁻⁻,1,one⁺⁺), s1 = (-1,0,1), s2 = (-1,1)])  # around faces

    @test normal(R*[1.1r1, 0], br) ≈ R*[1,0]
    @test normal(R*[-1.1r1, 0], br) ≈ R*[-1,0]
    @test normal(R*[0, 1.1r2], br) ≈ R*[0,1]
    @test normal(R*[0, -1.1r2], br) ≈ R*[0,-1]
    @test normal(R*[1.1r1, 1.01r2], br) ≈ R*(normalize([1.1r1, 1.01r2]-[r1,r2]))

    xmax = (R*[r1,r2])[1]
    ymax = (R*[-r1,r2])[2]
    @test bounds(br) ≈ (-[xmax,ymax], [xmax,ymax])
    @test checkbounds(br)

    @test (∆ = rand(2); translate(br,∆) ≈ Box([0,0]+∆, [2r1, 2r2], [ax1 ax2]))
end  # @testset "Box, rotated"

@testset "Box, skewed" begin
    ax1, ax2 = normalize.(([1,-1], [0,1]))
    r1, r2 = 1, 1  # "radii"
    bs = Box([0,0], [2r1, 2r2], [ax1 ax2])

    @test bs == deepcopy(bs)
    @test hash(bs) == hash(deepcopy(bs))

    n1, n2 = normalize.(([1,0], [1,1]))
    @test ((x,nout) = surfpt_nearby([0,0],bs); (x⋅n1≈1/√2 && nout≈n1) || (x⋅n2≈1/√2 && nout≈n2))  # handle point at center properly
    @test all([(p = (s1*r1*ax1+s2*r2*ax2); (x,nout) = surfpt_nearby(1.1p,bs); nout⋅(s1*n1+s2*n2)>0 && norm(nout)≈1) for s1 = (-1,1), s2 = (-1,1)])  # outside corners
    @test all([(p = (s1*r1*ax1+s2*r2*ax2); (x,nout) = surfpt_nearby(p,bs); x≈p && nout⋅(s1*n1+s2*n2)>0 && norm(nout)≈1) for s1 = (-1,1), s2 = (-1,1)])  # at corners
    # Use the above, less demanding tests instead of the below two; surfpt_nearby suffers from some inaccuracy around corners of skewed boxes.
    # @test all([(p = (s1*r1*ax1+s2*r2*ax2); (x,nout) = surfpt_nearby(1.1p,bs); all([s1*n1 s2*n2]'*nout.>0) && norm(nout)≈1) for s1 = (-1,1), s2 = (-1,1)])  # outside corners
    # @test all([(p = (s1*r1*ax1+s2*r2*ax2); (x,nout) = surfpt_nearby(p,bs); all([s1*n1 s2*n2]'*nout.≥0) && norm(nout)≈1) for s1 = (-1,1), s2 = (-1,1)])  # at corners
    @test all([(p1 = s1*r1*ax1; p2 = s2*r2/2*ax2; surfpt_nearby(ρ*p1+p2,bs) ≈ (p1+p2, s1*n1)) for ρ = (one⁻,1,one⁺), s1 = (-1,1), s2 = (-1,0,1)])  # around faces
    @test all([(p1 = s1*r1/2*ax1; p2 = s2*r2*ax2; surfpt_nearby(p1+ρ*p2,bs) ≈ (p1+p2, s2*n2)) for ρ = (one⁻,1,one⁺), s1 = (-1,0,1), s2 = (-1,1)])  # around faces

    @test norm(normal([0,1], bs)) ≈ 1

    xmax = (r1*ax1+r2*ax2)[1]
    ymax = (r2*ax2-r1*ax1)[2]
    @test bounds(bs) ≈ (-[xmax,ymax],[xmax,ymax])
    @test checkbounds(bs)

    @test (∆ = rand(2); translate(bs,∆) ≈ Box([0,0]+∆, [2r1, 2r2], [ax1 ax2]))
end  # @testset "Box, skewed"


text:
# ---
# title: 1081. Smallest Subsequence of Distinct Characters
# id: problem1081
# author: Tian Jun
# date: 2020-10-31
# difficulty: Medium
# categories: String, Stack, Greedy
# link: <https://leetcode.com/problems/smallest-subsequence-of-distinct-characters/description/>
# hidden: true
# ---
#
# Return the lexicographically smallest subsequence of `s` that contains all the
# distinct characters of `s` exactly once.
#
# **Note:** This question is the same as 316:
# <https://leetcode.comhttps://leetcode.com/problems/remove-duplicate-letters/>
#
#
#
# **Example 1:**
#
#
#
#     Input: s = "bcabc"
#     Output: "abc"
#
#
# **Example 2:**
#
#
#
#     Input: s = "cbacdcbc"
#     Output: "acdb"
#
#
#
#
# **Constraints:**
#
#   * `1 <= s.length <= 1000`
#   * `s` consists of lowercase English letters.
#
#
## @lc code=start
using LeetCode

## add your code here:
## @lc code=end


text:
using FLPprocess
using Test

@testset "FLPprocess.jl" begin
    # Write your own tests here.
end


text:
function allunique(A)
    for i in 1:length(A)
        for j in i+1:length(A)
            if A[i]==A[j]
                return false
            end
        end
    end
    return true
end


text:
module okex

using HTTP
using JSON
using Nettle
using Base64
using Dates
using Printf
using DataStructures

global const BASE_URL = "https://www.okex.com"

publickey() = ENV["OKEX-PUBLIC-KEY"]
secretkey() = ENV["OKEX-SECRET-KEY"]
passshare() = ENV["OKEX-PASSPHRASE"]

function public_request(
    method::String,
    request_path::String,
    query::OrderedDict
    )
    url = join([BASE_URL, request_path], "/")
    pairs = join(string.(keys(query),"=", values(query)), "&")
    curl = url * "?" * pairs

    response = HTTP.request(method, curl, query)
    json = JSON.parse(String(response.body))
    return json
end

public_request("GET", "api/spot/v3/instruments/BTC-USDT/book", OrderedDict("size"=> "50", "depth" => "0.1"))

function details()
    method = "GET"
    path = "api/spot/v3/instruments"

    result = public_request(method, path, OrderedDict())
    form =
        x -> (
            symbol = x["instrument_id"],
            base_currency = x["base_currency"],
            quote_currency = x["quote_currency"],
        )

    return map(form, result)
end

function stats_24hr(symbol::String = "ALL")
    method = "GET"

    if !isequal(symbol, "ALL")

        path = "api/spot/v3/instruments/$symbol/ticker"
        x = public_request(method, path, OrderedDict())

        return (
            symbol = x["instrument_id"],
            first_price = parse(Float64, x["open_24h"]),
            last_price = parse(Float64, x["last"]),
            high_price = parse(Float64, x["high_24h"]),
            low_price = parse(Float64, x["low_24h"]),
            base_volume = parse(Float64, x["base_volume_24h"]),
            quote_volume = parse(Float64, x["quote_volume_24h"]),
            time = x["timestamp"],
        )
    end

    path = "api/spot/v3/instruments/ticker"
    result = public_request(method, path, OrderedDict())

    form =
        x -> (
            symbol = x["instrument_id"],
            first_price = parse(Float64, x["open_24h"]),
            last_price = parse(Float64, x["last"]),
            high_price = parse(Float64, x["high_24h"]),
            low_price = parse(Float64, x["low_24h"]),
            base_volume = parse(Float64, x["base_volume_24h"]),
            quote_volume = parse(Float64, x["quote_volume_24h"]),
            time = x["timestamp"],
        )
    return map(form, result)
end

function order_book(symbol::String, size::Int = 200, depth::Float64 = 0.1)

    method = "GET"
    path = "api/spot/v3/instruments/$symbol/book"
    query = OrderedDict("size" => size, "depth" => depth)

    result = public_request(method, path, query)
    return result
end

function symbols()
    method = "GET"
    path = "api/spot/v3/instruments"

    result = public_request(method, path, OrderedDict())

    return  map(x -> x["instrument_id"], result)
end

function historical_trades(symbol::String, limit::Int = 100)
    method = "GET"
    path = "api/spot/v3/instruments/$symbol/trades"
    query = OrderedDict("limit" => limit)

    result = public_request(method, path, query)
    form =
        x -> (
            price = parse(Float64, x["price"]),
            size = parse(Float64, x["size"]),
            side = x["side"],
            time = x["time"],
            trade_id = parse(Int64, x["trade_id"]),
        )
    return map(form, result)
end

#period = [60/180/300/900/1800/3600/7200/14400/21600/43200/86400/604800] =>
#[1 minute, 3 minutes, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 6 hours, 12 hours, 1 day, 1 week,1 month ,3 months, 6 months and 1 year]
#max = 200

function klines(
    symbol::String,
    period::Int64 = 60, #1minute candle,
    start::Union{TimeType,Missing} = missing,
    finish::Union{TimeType,Missing} = missing,
)

    method = "GET"
    path = "api/spot/v3/instruments/$symbol/candles"
    format = "yyyy-mm-ddTHH:MM:SS.sss"

    form =
        x -> (
            open = parse(Float64, x[2]),
            high = parse(Float64, x[3]),
            low = parse(Float64, x[4]),
            close = parse(Float64, x[5]),
            candle_volume = parse(Float64, x[6]),
            time = DateTime(x[1][1:length(x[1])-1], format),
        )

    if isequal(start, missing) && isequal(finish, missing)
        query = OrderedDict("granularity" => period)
        result = public_request(method, path, query)
        return map(form, result)
    end

    query = OrderedDict(
        "granularity" => period,
        "start" => string(start) * "Z",
        "end" => string(finish) * "Z",
    )

    result = public_request(method, path, query)

    return map(form, result)
end


text:
using Term.Boxes
import Term: Segment

@testset "\e34mBOX" begin
    testbox = Term.Boxes.Box(
        "ASCII",
        """
        +--+
        | ||
        |-+|
        | ||
        |-+|
        |-+|
        | ||
        +--+
        """,
    )

    @test string(testbox) == "Box\e[2m(ASCII)\e[0m"

    @suppress_out begin
        @test_nothrow println(stdout, testbox)
    end

    _bstring = Term.Boxes.fit(Term.Boxes.ASCII, [1, 1, 1, 1, 1, 1, 1, 1])
    @test _bstring ==
          "+---------------+\n| | | | | | | | |\n|-+-+-+-+-+-+-+-|\n| | | | | | | | |\n|-+-+-+-+-+-+-+-|\n|-+-+-+-+-+-+-+-|\n| | | | | | | | |\n+---------------+"

    # test get row
    @test get_row(ROUNDED, [3], :top) == "╭───╮"
    @test get_row(ROUNDED, 3, :top) == "╭─╮"
    @test get_row(ROUNDED, [3, 5], :top) == "╭───┬─────╮"

    # test get title row with no justification
    tr = get_title_row(:top, Term.Boxes.ROUNDED, nothing; width = 22, style = "red")
    @test tr isa Segment
    @test tr.measure.w == 22

    # test title row with justification
    left = get_title_row(
        :top,
        Term.Boxes.ROUNDED,
        "test";
        width = 22,
        justify = :left,
        title_style = "blue",
        style = "red",
    )
    @test left.measure.w == 22
    @test left.text ==
          "\e[31m╭──── \e[34mtest\e[39m\e[31m\e[31m ──────────╮\e[39m\e[0m\e[39m\e[31m"

    right = get_title_row(
        :top,
        Term.Boxes.ROUNDED,
        "test";
        width = 22,
        justify = :right,
        title_style = "blue",
        style = "red",
    )
    @test right.measure.w == 22
    @test right.text ==
          "\e[31m╭─────────── \e[34mtest\e[39m\e[31m\e[31m ───╮\e[39m\e[0m\e[39m\e[31m"

    center = get_title_row(
        :top,
        Term.Boxes.ROUNDED,
        "test";
        width = 22,
        justify = :center,
        title_style = "blue",
        style = "red",
    )
    @test center.measure.w == 22
    @test center.text == "\e[31m╭───────\e[39m \e[34mtest\e[39m\e[31m ───────╮\e[39m\e[0m"

    for width in (15, 21, 33, 58), justify in (:left, :center, :right)
        line = get_title_row(
            :top,
            Term.Boxes.DOUBLE,
            "test";
            width = width,
            justify = justify,
            title_style = "on_green",
            style = "blue bold",
        )

        @test line.measure.w == width
    end
end


text:
include("transit_orb.jl")

function transit(t, x)
    nsub = 5
    n = length(t)
    flux::Array{Number, 1} = zeros(n)
    for i=1:n
        ti = t[i]
        if i > 1
            dt = ti-t[i-1]
        else
            dt = 1e-10
        end
        flux[i] = transit_orb(ti, x, dt, nsub)
    end
    return flux
end


text:
# This file is a part of Julia. License is MIT: https://julialang.org/license

# "is a null with type T", curried on 2nd argument
isnull_oftype(x::Nullable, T::Type) = eltype(x) == T && isnull(x)
isnull_oftype(T::Type) = x -> isnull_oftype(x, T)

# return true if nullables (or arrays of nullables) have the same type,
# nullity, and value (if they are non-null)
istypeequal(x::Nullable, y::Nullable) =
    typeof(x) == typeof(y) && isnull(filter(!, x .== y))
istypeequal(x::AbstractArray, y::AbstractArray) =
    length(x) == length(y) && all(xy -> istypeequal(xy...), zip(x, y))

types = [
    Bool,
    Float16,
    Float32,
    Float64,
    Int128,
    Int16,
    Int32,
    Int64,
    Int8,
    UInt16,
    UInt32,
    UInt64,
    UInt8,
]

# Nullable{T}() = new(true)
for T in types
    x = Nullable{T}()
    @test x.hasvalue === false
    @test isa(x.value, T)
    @test eltype(Nullable{T}) === T
    @test eltype(x) === T
end

# Nullable{T}(value::T) = new(false, value)
for T in types
    x = Nullable{T}(zero(T))
    @test x.hasvalue === true
    @test isa(x.value, T)
    @test x.value === zero(T)
    @test eltype(x) === T

    x = Nullable{T}(one(T))
    @test x.hasvalue === true
    @test isa(x.value, T)
    @test x.value === one(T)
    @test eltype(x) === T
end

# Nullable{T}(value::T, hasvalue::Bool) = new(hasvalue, value)
for T in types
    x = Nullable{T}(zero(T), true)
    @test x.hasvalue === true
    @test isa(x.value, T)
    @test x.value === zero(T)
    @test eltype(x) === T

    x = Nullable{T}(zero(T), false)
    @test x.hasvalue === false
    @test isa(x.value, T)
    @test eltype(Nullable{T}) === T
    @test eltype(x) === T
end

# struct NullException <: Exception
@test isa(NullException(), NullException)
@test_throws NullException throw(NullException())

# Nullable{T}(value::T) = Nullable{T}(value)
for T in types
    v = zero(T)
    x = Nullable(v)
    @test x.hasvalue === true
    @test isa(x.value, T)
    @test x.value === v

    v = one(T)
    x = Nullable(v)
    @test x.hasvalue === true
    @test isa(x.value, T)
    @test x.value === v
end

# show{T}(io::IO, x::Nullable{T})
io1 = IOBuffer()
io2 = IOBuffer()
for (i, T) in enumerate(types)
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))
    show(io1, x1)
    @test String(take!(io1)) == @sprintf("Nullable{%s}()", T)
    show(io1, x2)
    showcompact(io2, get(x2))
    @test String(take!(io1)) == @sprintf("Nullable{%s}(%s)", T, String(take!(io2)))
    show(io1, x3)
    showcompact(io2, get(x3))
    @test String(take!(io1)) == @sprintf("Nullable{%s}(%s)", T, String(take!(io2)))
end

module NullableTestEnum
import Test
# For curmod_*
include("testenv.jl")
io = IOBuffer()
@enum TestEnum a b
show(io, Nullable(a))
Test.@test String(take!(io)) == "Nullable{$(curmod_prefix)TestEnum}(a)"
end

# showcompact(io::IO, x::Nullable)
io1 = IOBuffer()
io2 = IOBuffer()
for (i, T) in enumerate(types)
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))
    showcompact(io1, x1)
    @test String(take!(io1)) == "#NULL"
    showcompact(io1, x2)
    showcompact(io2, get(x2))
    @test String(take!(io1)) == String(take!(io2))
    showcompact(io1, x3)
    showcompact(io2, get(x3))
    @test String(take!(io1)) == String(take!(io2))

    a1 = [x2]
    showcompact(io1, a1)
    showcompact(io2, x2)
    @test String(take!(io1)) ==
        @sprintf("Nullable{%s}[%s]", string(T), String(take!(io2)))
end

# get(x::Nullable)
for T in types
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))

    @test_throws NullException get(x1)
    @test get(x2) === zero(T)
    @test get(x3) === one(T)
end

@test_throws NullException get(Nullable())

# get{S, T}(x::Nullable{S}, y::T)
for T in types
    x0 = Nullable()
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))

    @test get(x0, zero(T)) === zero(T)
    @test get(x0, one(T)) === one(T)
    @test get(x1, zero(T)) === zero(T)
    @test get(x1, one(T)) === one(T)
    @test get(x2, one(T)) === zero(T)
    @test get(x3, zero(T)) === one(T)
end

for T in types
    # unsafe_get(x::Nullable)
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))
    a = rand(T)
    x4 = Nullable(a)

    @test isa(unsafe_get(x1), T)
    @test unsafe_get(x2) === zero(T)
    @test unsafe_get(x3) === one(T)
    @test unsafe_get(x4) === a

    # unsafe_get(x)
    x2 = zero(T)
    x3 = one(T)
    x4 = rand(T)

    @test unsafe_get(x2) === zero(T)
    @test unsafe_get(x3) === one(T)
    @test unsafe_get(x4) === x4
end

@test_throws UndefRefError unsafe_get(Nullable())
@test_throws UndefRefError unsafe_get(Nullable{String}())
@test_throws UndefRefError unsafe_get(Nullable{Array}())

for T in types
    # isnull(x::Nullable)
    x1 = Nullable{T}()
    x2 = Nullable(zero(T))
    x3 = Nullable(one(T))

    @test isnull(x1) === true
    @test isnull(x2) === false
    @test isnull(x3) === false

    # isnull(x)
    x1 = zero(T)
    x2 = one(T)
    x3 = rand(T)

    @test isnull(x1) === false
    @test isnull(x2) === false
    @test isnull(x3) === false
end

@test isnull(Nullable())

# function =={S, T}(x::Nullable{S}, y::Nullable{T})
for T in types
    x0 = Nullable()
    x1 = Nullable{T}()
    x2 = Nullable{T}()
    x3 = Nullable(zero(T))
    x4 = Nullable(one(T))

    @test_throws NullException (x0 == x1)
    @test_throws NullException (x0 == x2)
    @test_throws NullException (x0 == x3)
    @test_throws NullException (x0 == x4)

    @test_throws NullException (x1 == x1)
    @test_throws NullException (x1 == x2)
    @test_throws NullException (x1 == x3)
    @test_throws NullException (x1 == x4)

    @test_throws NullException (x2 == x1)
    @test_throws NullException (x2 == x2)
    @test_throws NullException (x2 == x3)
    @test_throws NullException (x2 == x4)

    @test_throws NullException (x3 == x1)
    @test_throws NullException (x3 == x2)
    @test_throws NullException (x3 == x3)
    @test_throws NullException (x3 == x4)

    @test_throws NullException (x4 == x1)
    @test_throws NullException (x4 == x2)
    @test_throws NullException (x4 == x3)
    @test_throws NullException (x4 == x4)
end

# function hash(x::Nullable, h::UInt)
for T in types
    x0 = Nullable()
    x1 = Nullable{T}()
    x2 = Nullable{T}()
    x3 = Nullable(zero(T))
    x4 = Nullable(one(T))

    @test isa(hash(x0), UInt)
    @test isa(hash(x1), UInt)
    @test isa(hash(x2), UInt)
    @test isa(hash(x3), UInt)
    @test isa(hash(x4), UInt)

    @test hash(x0) == hash(x2)
    @test hash(x0) != hash(x3)
    @test hash(x0) != hash(x4)
    @test hash(x1) == hash(x2)
    @test hash(x1) != hash(x3)
    @test hash(x1) != hash(x4)
    @test hash(x2) != hash(x3)
    @test hash(x2) != hash(x4)
    @test hash(x3) != hash(x4)
end

mutable struct TestNType{T}
    v::Nullable{T}
end

for T in types
    x1 = TestNType{T}(Nullable{T}())
    @test isnull(x1.v)
    x1.v = one(T)
    @test !isnull(x1.v)
    @test get(x1.v, one(T)) === one(T)
end

# Operators
TestTypes = [[T.parameters[1] for T in Base.uniontypes(Base.NullSafeTypes)];
             [BigInt, BigFloat,
              Complex{Int}, Complex{Float64}, Complex{BigFloat},
              Rational{Int}, Rational{BigInt}]]
for S in TestTypes, T in TestTypes
    u0 = zero(S)
    u1 = one(S)
    if S <: AbstractFloat
        u2 = S(NaN)
    elseif S <: Complex && S.parameters[1] <: AbstractFloat
        u2 = S(NaN, NaN)
    else
        u2 = u1
    end

    v0 = zero(T)
    v1 = one(T)
    if T <: AbstractFloat
        v2 = T(NaN)
    elseif T <: Complex && T.parameters[1] <: AbstractFloat
        v2 = T(NaN, NaN)
    else
        v2 = v1
    end

    for u in (u0, u1, u2), v in (v0, v1, v2)
        # function isequal(x::Nullable, y::Nullable)
        @test isequal(Nullable(u), Nullable(v)) === isequal(u, v)
        @test isequal(Nullable(u), Nullable(u)) === true
        @test isequal(Nullable(v), Nullable(v)) === true

        @test isequal(Nullable(u), Nullable(v, false)) === false
        @test isequal(Nullable(u, false), Nullable(v)) === false
        @test isequal(Nullable(u, false), Nullable(v, false)) === true

        @test isequal(Nullable(u), Nullable{T}()) === false
        @test isequal(Nullable{S}(), Nullable(v)) === false
        @test isequal(Nullable{S}(), Nullable{T}()) === true

        @test isequal(Nullable(u), Nullable()) === false
        @test isequal(Nullable(), Nullable(v)) === false
        @test isequal(Nullable{S}(), Nullable()) === true
        @test isequal(Nullable(), Nullable{T}()) === true
        @test isequal(Nullable(), Nullable()) === true

        # function isless(x::Nullable, y::Nullable)
        if S <: Real && T <: Real
            @test isless(Nullable(u), Nullable(v)) === isless(u, v)
            @test isless(Nullable(u), Nullable(u)) === false
            @test isless(Nullable(v), Nullable(v)) === false

            @test isless(Nullable(u), Nullable(v, false)) === true
            @test isless(Nullable(u, false), Nullable(v)) === false
            @test isless(Nullable(u, false), Nullable(v, false)) === false

            @test isless(Nullable(u), Nullable{T}()) === true
            @test isless(Nullable{S}(), Nullable(v)) === false
            @test isless(Nullable{S}(), Nullable{T}()) === false

            @test isless(Nullable(u), Nullable()) === true
            @test isless(Nullable(), Nullable(v)) === false
            @test isless(Nullable{S}(), Nullable()) === false
            @test isless(Nullable(), Nullable{T}()) === false
            @test isless(Nullable(), Nullable()) === false
        end
    end
end

# issue #9462
for T in types
    @test isa(convert(Nullable{Number}, Nullable(one(T))), Nullable{Number})
    @test isa(convert(Nullable{Number}, one(T)), Nullable{Number})
    @test isa(convert(Nullable{T}, one(T)), Nullable{T})
    @test isa(convert(Nullable{Any}, Nullable(one(T))), Nullable{Any})
    @test isa(convert(Nullable{Any}, one(T)), Nullable{Any})

    # one(T) is convertible to every type in types
    # let's test that with Nullables
    for S in types
        @test isa(convert(Nullable{T}, one(S)), Nullable{T})
    end
end

@test isnull(convert(Nullable, nothing))
@test isnull(convert(Nullable{Int}, nothing))
@test isa(convert(Nullable{Int}, nothing), Nullable{Int})

@test convert(Nullable, 1) === Nullable(1)
@test convert(Nullable, Nullable(1)) === Nullable(1)
@test isequal(convert(Nullable, "a"), Nullable("a"))
@test isequal(convert(Nullable, Nullable("a")), Nullable("a"))

using Dates
@test promote_type(Nullable{Int}, Int) === Nullable{Int}
@test promote_type(Nullable{Union{}}, Int) === Nullable{Int}
@test promote_type(Nullable{Float64}, Nullable{Int}) === Nullable{Float64}
@test promote_type(Nullable{Union{}}, Nullable{Int}) === Nullable{Int}
@test promote_type(Nullable{Date}, Nullable{DateTime}) === Nullable{DateTime}

@test Base.promote_op(+, Nullable{Int}, Nullable{Int}) == Nullable{Int}
@test Base.promote_op(-, Nullable{Int}, Nullable{Int}) == Nullable{Int}
@test Base.promote_op(+, Nullable{Float64}, Nullable{Int}) == Nullable{Float64}
@test Base.promote_op(-, Nullable{Float64}, Nullable{Int}) == Nullable{Float64}
@test Base.promote_op(-, Nullable{DateTime}, Nullable{DateTime}) == Nullable{Dates.Millisecond}

# tests for istypeequal (which uses filter, broadcast)
@test istypeequal(Nullable(0), Nullable(0))
@test !istypeequal(Nullable(0), Nullable(0.0))
@test !istypeequal(Nullable(0), Nullable(1))
@test !istypeequal(Nullable(0), Nullable(1.0))
@test istypeequal([Nullable(0), Nullable(1)], [Nullable(0), Nullable(1)])
@test istypeequal([Nullable(0), Nullable(1)], Any[Nullable(0), Nullable(1)])
@test !istypeequal([Nullable(0), Nullable(1)], Any[Nullable(0.0), Nullable(1)])
@test !istypeequal([Nullable(0), Nullable(1)], [Nullable(0), Nullable(2)])
@test !istypeequal([Nullable(0), Nullable(1)],
                   [Nullable(0), Nullable(1), Nullable(2)])

# filter
for p in (_ -> true, _ -> false)
    @test @inferred(filter(p, Nullable()))      |> isnull_oftype(Union{})
    @test @inferred(filter(p, Nullable{Int}())) |> isnull_oftype(Int)
end
@test @inferred(filter(_ -> true, Nullable(85)))  === Nullable(85)
@test @inferred(filter(_ -> false, Nullable(85))) |> isnull_oftype(Int)
@test @inferred(filter(x -> x > 0, Nullable(85))) === Nullable(85)
@test @inferred(filter(x -> x < 0, Nullable(85))) |> isnull_oftype(Int)
@test get(@inferred(filter(x -> length(x) > 2, Nullable("test")))) == "test"
@test @inferred(filter(x -> length(x) > 5, Nullable("test"))) |>
    isnull_oftype(String)

# map
sqr(x) = x^2
@test @inferred(map(sqr, Nullable()))        |> isnull_oftype(Union{})
@test @inferred(map(sqr, Nullable{Int}()))   |> isnull_oftype(Int)
@test @inferred(map(sqr, Nullable(2)))       === Nullable(4)
@test @inferred(map(+, Nullable(0.0)))       === Nullable(0.0)
@test @inferred(map(+, Nullable(3.0, false)))=== Nullable(3.0, false)
@test @inferred(map(-, Nullable(1.0)))       === Nullable(-1.0)
@test @inferred(map(-, Nullable{Float64}())) |> isnull_oftype(Float64)
@test @inferred(map(sin, Nullable(1)))       === Nullable(sin(1))
@test @inferred(map(sin, Nullable{Int}()))   |> isnull_oftype(Float64)

# should not throw if function wouldn't be called
@test map(x -> x ? 0 : 0.0, Nullable())       |> isnull_oftype(Union{})
@test map(x -> x ? 0 : 0.0, Nullable(true))   === Nullable(0)
@test map(x -> x ? 0 : 0.0, Nullable(false))  === Nullable(0.0)
@test map(x -> x ? 0 : 0.0, Nullable{Bool}()) |> isnull_oftype(Union{})

# broadcast and elementwise
@test sin.(Nullable(0.0))            === Nullable(0.0)
@test sin.(Nullable{Float64}())      |> isnull_oftype(Float64)
@test @inferred(broadcast(sin, Nullable(0.0)))       === Nullable(0.0)
@test @inferred(broadcast(sin, Nullable{Float64}())) |> isnull_oftype(Float64)

@test Nullable(8) .+ Nullable(10)     === Nullable(18)
@test Nullable(8) .- Nullable(10)     === Nullable(-2)
@test Nullable(8) .+ Nullable{Int}()  |> isnull_oftype(Int)
@test Nullable{Int}() .- Nullable(10) |> isnull_oftype(Int)

@test @inferred(broadcast(log, 10, Nullable(1.0))) ===
      Nullable(0.0)
@test @inferred(broadcast(log, 10, Nullable{Float64}())) |>
      isnull_oftype(Float64)
@test @inferred(broadcast(log, Nullable(10), Nullable(1.0))) ===
      Nullable(0.0)
@test @inferred(broadcast(log, Nullable(10), Nullable{Float64}())) |>
      isnull_oftype(Float64)

@test Nullable(2) .^ Nullable(4)      === Nullable(16)
@test Nullable(2) .^ Nullable{Int}()  |> isnull_oftype(Int)

# multi-arg broadcast
@test (Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable(1) .+
       Nullable(1) === Nullable(6))
@test (Nullable(1) .+ Nullable(1) .+ Nullable(1) .+ Nullable{Int}() .+
       Nullable(1) .+ Nullable(1) |> isnull_oftype(Int))

# these are not inferrable because there are too many arguments
us = map(Nullable, 1:20)
@test broadcast(max, us...) === Nullable(20)
@test isnull(broadcast(max, us..., Nullable{Int}()))

# test all elementwise operations
# note that elementwise operations are the same as broadcast
for op in (+, -, *, /, \, //, ==, <, !=, <=, ÷, %, <<, >>, ^)
    # op(1, 1) chosen because it works for all operations
    res = op(1, 1)
    @test @inferred(broadcast(op, Nullable(1), Nullable(1)))         ===
          Nullable(res)
    @test @inferred(broadcast(op, Nullable{Int}(), Nullable(1)))     |>
          isnull_oftype(typeof(res))
    @test @inferred(broadcast(op, Nullable(1), Nullable{Int}()))     |>
          isnull_oftype(typeof(res))
    @test @inferred(broadcast(op, Nullable{Int}(), Nullable{Int}())) |>
          isnull_oftype(typeof(res))
    @test @inferred(broadcast(op, Nullable(1), 1))                   ===
          Nullable(res)
    @test @inferred(broadcast(op, 1, Nullable(1)))                   ===
          Nullable(res)
end

# test reasonable results for Union{}
# the exact types of these is finnicky and depends on implementation details
# but is guaranteed to be at worst concrete and possibly Union{} on a good day
@test isnull(@inferred(Nullable() .+ Nullable()))
@test isnull(@inferred(Nullable() .+ 1))
@test isnull(@inferred(Nullable() .+ Nullable(1)))

# test that things don't pessimize because of non-homogenous types
@test Nullable(10.5) ===
    @inferred(broadcast(+, 1, 2, Nullable(3), Nullable(4.0), Nullable(1//2)))

# test fast path taken
for op in (+, *, -)
    for b1 in (false, true)
        for b2 in (false, true)
            @test Nullable{Int}(op(1, 2), b1 & b2) ===
                @inferred(broadcast(op, Nullable{Int}(1, b1),
                                        Nullable{Int}(2, b2)))
        end
    end
end

# issue #11675
@test repr(Nullable()) == "Nullable{Union{}}()"

# issue #19270
let f19270(x::S, y::T) where {S,T} = Base.promote_op(^, S, T)
    @test f19270(Nullable(0.0f0), Nullable(BigInt(0))) == Nullable{Float32}
end

# issue #21397
@test Nullable(Tuple) === Nullable{DataType}(Tuple)

max_stars_repo_name: JuliaLangEs/julieta

max_issues_repo_name: JuliaLangEs/julieta

max_forks_repo_name: JuliaLangEs/julieta