// noinspection JSPotentiallyInvalidUsageOfThis

import {invokeCallback, mixin} from "@/common.js";
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";
import {unique} from "@/misc.js";
import {allocationText, useChartOrderStore} from "@/orderbuild.js";
import Color from "color";


//
// Usage of Shapes:
// const shape = new Shape(ShapeType.HLine)
// shape.draw()
// shape.points
// > [{time:17228394, price:42638.83}]
//
// shape.model stores the shape-specific control points, as defined by the shape subclass, and may be a vue ref.
// Use the shape.model.* fields in components to get reactive effects with the chart, or pass an onModel callback
// to the constructor
//


export const ShapeType = {
    // this "enum" is a record of the TradingView keystrings

    /*
    The `name` field must match the user-facing name in the TradingView toolbar UI
    TradingView drawing tool codes:
    https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library/#supportedlinetools
    text anchored_text note anchored_note signpost double_curve arc icon emoji sticker arrow_up arrow_down arrow_left arrow_right price_label price_note arrow_marker flag vertical_line horizontal_line cross_line horizontal_ray trend_line info_line trend_angle arrow ray extended parallel_channel disjoint_angle flat_bottom anchored_vwap pitchfork schiff_pitchfork_modified schiff_pitchfork balloon comment inside_pitchfork pitchfan gannbox gannbox_square gannbox_fixed gannbox_fan fib_retracement fib_trend_ext fib_speed_resist_fan fib_timezone fib_trend_time fib_circles fib_spiral fib_speed_resist_arcs fib_channel xabcd_pattern cypher_pattern abcd_pattern callout triangle_pattern 3divers_pattern head_and_shoulders fib_wedge elliott_impulse_wave elliott_triangle_wave elliott_triple_combo elliott_correction elliott_double_combo cyclic_lines time_cycles sine_line long_position short_position forecast date_range price_range date_and_price_range bars_pattern ghost_feed projection rectangle rotated_rectangle circle ellipse triangle polyline path curve cursor dot arrow_cursor eraser measure zoom brush highlighter regression_trend fixed_range_volume_profile
    */
    Segment: {name: 'Trend Line', code: 'trend_line'},
    Ray: {name: 'Ray', code: 'ray'},
    Line: {name: 'Extended Line', code: 'extended'},
    HRay: {name: 'Horizontal Ray', code: 'horizontal_ray'},
    HLine: {name: 'Horizontal Line', code: 'horizontal_line', drawingProp: 'linetoolhorzline'},
    VLine: {name: 'Vertical Line', code: 'vertical_line'},
    PriceRange: {name: 'Price Range', code: 'price_range'},
}


export class Shape {

    constructor(type, onModel=null, onDelete=null, props=null) {
        // the Shape object manages synchronizing internal data with a corresponding TradingView shape

        // each shape in the class hierarchy overrides setModel() to cause effects in TradingView
        // TV callbacks are returned to the on*() handlers, primarily onPoints() and onProps(). Handlers
        // of TV callbacks should call updateModel() with just the kv's they want to change. Any changes
        // are then passed onto the onModel() callback.

        // the model object has various attributes defined by each shape subclass.  each subclass's constructor
        // must call setModel(model) after first calling super.constructor() and optionally declaring any model
        // defaults on this.model.

        // Shape: {
        //   lineColor, textColor, color, // lineColor and textColor default to color
        // }

        this.debug = false
        this.id = null // TradingView shapeId, or null if no TV shape created yet (drawing mode)
        this.type = type  // ShapeType
        this.model = {}  // set to nothing at first
        this.ourPoints = null
        this.tvPoints = null
        this.ourProps = {}
        if (props !== null)
            this.ourProps = mixin(props, this.ourProps)
        this.tvProps = null
        if (onModel !== null)
            this.onModel = onModel
        if (onDelete !== null )
            this.onDelete = onDelete

        //
        // Model values handled by this base class
        //

        this.model.color = null
        // if allocation is set, a percentage is displayed
        this.model.allocation = null
        // if maxAllocation is set, the line color (but not text color) is shaded down based on allocation
        this.model.maxAllocation = null
        // both amount and amountSymbol must be set in order to display amount text
        this.model.amount = null
        this.model.amountSymbol = null

        // LEAF SUBCLASSES MUST CALL setModel(model) AFTER ALL CONSTRUCTION.
    }

    //
    // primary interface methods
    //

    setModel(model) {
        // console.log('shape setModel', model)

        if (model.color)
            this.model.color = model.color
        if (model.allocation !== null && model.allocation !== undefined)
            this.model.allocation = model.allocation
        if (model.maxAllocation !== null && model.maxAllocation !== undefined)
            this.model.maxAllocation = model.maxAllocation
        if (model.amount !== null && model.amount !== undefined)
            this.model.amount = model.amount
        if (model.amountSymbol)
            this.model.amountSymbol = model.amountSymbol

        const newProps = {}

        // text color
        const color = this.model.color ? this.model.color : '#0044ff'
        newProps.textcolor = color

        // line color
        if (this.model.allocation && this.model.maxAllocation) {
            const w = this.model.allocation / this.model.maxAllocation
            if (!w)
                newProps.linecolor = 'rgba(0,0,0,0)'
            else {
                // weighted line color
                let c = new Color(color).rgb()
                // the perceptual color of a weight follows Stevens's Power Law
                // https://en.wikipedia.org/wiki/Stevens's_power_law
                newProps.linecolor = c.alpha(Math.pow(w,0.67)).string()
            }
        }
        else
            newProps.linecolor = color

        // text label
        const text = allocationText(this.model.allocation, this.model.amount, this.model.amountSymbol)
        if (!text.length)
            newProps.showLabel = false
        else {
            newProps.text = text
            newProps.showLabel = true
        }

        if (this.debug && this.id)
            console.log('newProps', chart.getShapeById(this.id).getProperties(), newProps)
        this.setProps(newProps)
    }

    onModel(model, changedKeys) {}  // model was changed by a TradingView user action

    updateModel(changes) {
        if (this.debug) console.log('updateModel', this.id, changes)
        const changedKeys = []
        for (const k of Object.keys(changes)) {
            if( changes[k] !== undefined && changes[k] !== this.model[k] ) {
                changedKeys.push(k)
                this.model[k] = changes[k]
            }
        }
        if (changedKeys.length)
            this.onModel(this.model, changedKeys)
    }

    colorProps() {
        if (!this.model.color&&!this.model.lineColor&&!this.model.textColor) return null
        const o = {}
        const p = this.type.drawingProp
        const lc = this.model.lineColor ? this.model.lineColor : this.model.color;
        const tc = this.model.textColor ? this.model.textColor : this.model.color;
        if (lc)
            o[p+".linecolor"] = lc
        if (tc)
            o[p+".textcolor"] = tc
        return o
    }

    draw() {
        // have the user draw this shape
        if (this.debug) console.log(`draw ${this.type.name}`, this.model)
        if (this.id)
            throw Error(`Shape already exists ${this.id}`)
        const or = this.drawingOverrides();
        // if (this.debug) console.log('drawing overrides', or)
        widget.applyOverrides(or)
        drawShape(this.type, new ShapeTVCallbacks(this))
    }

    // return an object with property defaults, e.g. { "linetoolhorzline.linecolor": "#7f11e0" }
    // https://www.tradingview.com/charting-library-docs/latest/api/modules/Charting_Library#drawingoverrides
    drawingOverrides() {
        return this.colorProps()
    }

    create() {
        if (this.id !== null) return
        // programatically create the shape using the current this.points
        if( this.ourPoints && this.ourPoints.length ) {
            this.doCreate()
        }
    }

    doCreate() {
        // createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
        this.tvPoints = [...this.ourPoints]
        createShape(this.type, this.ourPoints, {overrides:this.ourProps}, new ShapeTVCallbacks(this))
        if (this.debug) console.log('created', this.type.name, this.ourPoints, this.id)
    }

    createOrDraw() {
        if(this.id) return
        if(this.ourPoints && this.ourPoints.length)
            this.doCreate(this.ourPoints)
        else
            this.draw()
    }


    tvShape() {
        return this.id === null ? null : chart.getShapeById(this.id);
    }


    setPoints(points) {
        // setting points to null will delete the shape from the chart. setting points to a valid value will cause the
        // shape to be drawn.
        if (this.debug) console.log('setPoints', points, this.id)
        this.ourPoints = points
        if (points === null || !points.length)
            this.delete()
        else {
            if (this.id === null)
                this.doCreate()
            else if (dirtyPoints(this.tvPoints, points)) {
                const s = this.tvShape();
                if (this.debug) console.log('adjusting tv points', s, this.tvPoints, points)
                if (!dragging) {
                    if (this.debug) console.log('not dragging.  use setPoints.')
                    s.setPoints(points)
                }
                else {
                    if (this.debug) console.log('dragging.  use QUIET setPoints.')
                    // quiet setPoints doesn't disturb tool editing mode
                    const i = s._pointsConverter.apiPointsToDataSource(points)
                    // s._model.startChangingLinetool(this._source)
                    s._model.changeLinePoints(s._source, i)
                    // s._model.endChangingLinetool(!0)
                    s._source.createServerPoints()
                }
            }
        }
    }

    onPoints(points) {} // the control points of an existing shape were changed

    setProps(props) {
        if (!props || Object.keys(props).length===0) return
        if (this.debug) console.log('setProps', props)
        this.ourProps = mixin(props, this.ourProps)
        if(this.id) {
            const p = dirtyItems(this.tvProps, props)
            this.tvProps = this.tvProps === null ? p : mixin(p, this.tvProps)
            if (this.debug) console.log('setting tv props', this.tvProps)
            this.tvShape().setProperties(p)
        }
    }

    onProps(props) {  // the display properties of an existing shape were changed
        if (this.debug) console.log('shape onProps', this.model, props)
        if (props.textcolor && props.textcolor !== this.tvProps.textcolor)
            this.updateModel({color:props.textcolor})
        else if (props.linecolor && props.linecolor !== this.tvProps.linecolor)
            this.updateModel({color:props.linecolor})
    }

    beingDragged() {
        return draggingShapeIds.indexOf(this.id) !== -1
    }


    delete() {
        if (this.debug) console.log('shape.delete', this.id)
        this.ourPoints = null
        if (this.id === null) return
        deleteShapeId(this.id)
        this.id = null
    }


    //
    // Overridable shape callbacks initiated by TradingView
    //

    onDraw() {}  // start drawing a new shape for the builder
    onRedraw() {}  // the mouse moved while in drawing mode
    onUndraw() {}  // drawing was canceled by clicking on a different tool
    onAddPoint() {}  // the user clicked a point while drawing (that point is added to the points list)
    onCreate(points, props) {} // the user has finished creating all the control points.  drawing mode is exited and the initial shape is created.
    onMove(points) {} // the shape was moved by dragging a drawing element not the control point
    onDrag(points) {}
    onHide(props) {}
    onShow(props) {}
    onClick() {}  // the shape was selected
    onDelete() {}

}

function dirtyPoints(pointsA, pointsB) {
    if (pointsB===null)
        return pointsA !== null
    if (pointsA===null)
        return pointsB.length > 0
    if (pointsA.length!==pointsB.length)
        return true
    for (const i in pointsA) {
        const a = pointsA[i]
        const b = pointsB[i]
        if ( a.time !== b.time || a.price !== b.price )
            return true
    }
    return false
}

// B is modifying A
function dirtyKeys(propsA, propsB) {
    if (propsB===null)
        return propsA === null ? [] : [...Object.keys(propsA)]
    if (propsA===null)
        return [...Object.keys(propsB)]
    const keys = unique([...Object.keys(propsA), ...Object.keys(propsB)])
    return keys.filter((k)=> propsB[k] !== undefined && (!(k in propsA) || propsA[k] !== propsB[k]))
}


function dirtyItems(a, b) {
    const result = {}
    for (const k of dirtyKeys(a,b))
        result[k] = b[k]
    return result
}


class ShapeTVCallbacks {
    // These methods are called by TradingView and provide some default handling before invoking our own Shape callbacks

    constructor(shape) {
        this.shape = shape
        this.creating = false
    }

    onCreate(shapeId, _tvShape, points, props) {
        this.creating = true
        this.shape.id = shapeId
        invokeCallback(this.shape, 'onCreate', points, props)
    }

    onPoints(shapeId, _tvShape, points) {
        if (this.shape.debug) console.log('tvcb onPoints', points)
        this.shape.tvPoints = points
        this.shape.onPoints(points)
    }

    onProps(shapeId, _tvShape, props) {
        if (this.shape.debug) console.log('tvOnProps', props)
        if (this.creating) { // todo still useful?
            this.creating = false
            return
        }
        this.shape.tvProps = props
        this.shape.onProps(props)
    }

    onDraw() {
        invokeCallback(this.shape, 'onDraw')
    }

    onRedraw() {
        invokeCallback(this.shape, 'onRedraw')
    }

    onUndraw() {
        invokeCallback(this.shape, 'onUndraw')
    }

    onAddPoint() {
        invokeCallback(this.shape, 'onAddPoint')
    }

    onMove(_shapeId, _tvShape, points) {
        invokeCallback(this.shape, 'onMove',points)
    }

    onDrag(_shapeId, _tvShape, points) {
        if (this.shape.debug) console.log('onDrag')
        invokeCallback(this.shape, 'onDrag', points)
    }

    onHide(_shapeId, _tvShape, props) {
        invokeCallback(this.shape, 'onHide',props)
    }

    onShow(_shapeId, _tvShape, props) {
        invokeCallback(this.shape, 'onShow',props)
    }

    onClick(_shapeId, _tvShape) {
        invokeCallback(this.shape, 'onClick')
    }

    onDelete(shapeId) {
        this.shape.id = null
        invokeCallback(this.shape, 'onDelete', shapeId)
    }
}


export class Line extends Shape {
    onDrag(points) {
        const s = this.tvShape();
        if (this.debug) {
            console.log('shape', s)
            console.log('currentMovingPoint', s._source.currentMovingPoint())
            console.log('startMovingPoint', s._source.startMovingPoint())
            console.log('isBeingEdited', s._source.isBeingEdited())
            console.log('state', s._source.state())
        }
    }
}


export class HLine extends Line {
    constructor(model, onModel=null, onDelete=null, props=null) {
        super(ShapeType.HLine, onModel, onDelete, props)

        // Model
        this.model.price = null

        this.setModel(model) // call setModel at the end
    }

    delete() {
        this.model.price = null
        super.delete()
    }


    setModel(model) {
        super.setModel(model)
        if (model.price === null) {
            this.model.price = null
            this.delete()
        }
        else if (model.price !== this.model.price) {
            this.model.price = model.price
            this.setPoints([{time:0,price:this.model.price}])
        }
    }

    onPoints(points) {
        super.onPoints(points);
        const price = points[0].price;
        this.updateModel({price})
    }

}


function timeAdjustmentTooSmall(orig, newValue) {
    // TradingView adjusts our lines to be at the start of the intervals, so
    // we ignore deltas smaller than one interval prior
    return newValue === undefined ||
        orig !== null && orig !== undefined && newValue !== null &&
        newValue < orig && orig - newValue < useChartOrderStore().intervalSecs
}


function nearestOhlcStart(time) {
    const period = useChartOrderStore().intervalSecs
    return Math.round(time/period) * period
}


export class VLine extends Line {
    constructor(model, onModel=null, onDelete=null, props=null) {
        super(ShapeType.VLine, onModel, onDelete, props)

        // Model
        this.model.time = null
        this.debug=true

        this.setModel(model) // call setModel at the end
    }

    delete() {
        this.model.time = null
        super.delete()
    }


    setModel(model) {
        super.setModel(model)
        if (model.time === null) {
            this.model.time = null
            this.delete()
        }
        else {
            this.model.time = model.time
            const time = nearestOhlcStart(model.time);
            this.setPoints([{time, price:0}])
        }
    }

    onPoints(points) {
        super.onPoints(points);
        const orig = this.ourPoints && this.ourPoints.length ? this.ourPoints[0].time : null
        const time = points[0].time;
        if (!timeAdjustmentTooSmall(orig, time)) {
            this.updateModel({time: time})
        }
    }

}

