// noinspection JSPotentiallyInvalidUsageOfThis

import {invokeCallback, mixin} from "@/common.js";
import {chart, createShape, deleteShapeId, dragging, draggingShapeIds, drawShape, widget} from "@/charts/chart.js";


//
// 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'},
}


class Shape {
    constructor(type, model={}, onModel=null, onDelete=null, props=null) {
        // the Shape object manages synchronizing internal data with a corresponding TradingView shape
        this.id = null // TradingView shapeId, or null if no TV shape created yet (drawing mode)
        this.type = type  // ShapeType
        this.model = model  // subclass-specific
        this.points = null
        this.points = this.pointsFromModel()
        // console.log('construct points', this.points)
        this.props = props === null ? this.propsFromModel() : mixin(props, this.propsFromModel())
        if (onModel !== null)
            this.onModel = onModel
        if (onDelete !== null )
            this.onDelete = onDelete
        this.create()
    }

    //
    // primary interface methods
    //

    draw() {
        // have the user draw this shape
        console.log(`draw ${this.type.name}`, this.model)
        if (this.id)
            throw Error(`Shape already exists ${this.id}`)
        const or = this.drawingOverrides();
        // 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() {
        if (this.model.color===null) return null
        const o = {}
        const p = this.type.drawingProp
        o[p+".linecolor"] = this.model.color
        o[p+".textcolor"] = this.model.color
        return o
    }

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

    doCreate() {
        createShape(this.type, this.points, {overrides:this.props}, new ShapeTVCallbacks(this))
    }

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


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


    setModel(model, props=null) {
        for( const [k,v] of Object.entries(model))
            this.model[k] = v
        this.setPointsIfDirty(this.pointsFromModel());
        let p
        const mp = this.propsFromModel();
        if (props===null) {
            p = mp
            if (p===null)
                return
        }
        else if (mp !== null)
            p = mixin(props, mp)
        this.setPropsIfDirty(p)
    }


    setPoints(points) {
        this.points = points
        if (points === null || !points.length)
            this.delete()
        else {
            if (this.id === null)
                this.doCreate()
            else {
                const s = this.tvShape();
                if (!dragging) {
                    s.setPoints(points)
                }
                else {
                    // 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()
                }
            }
        }
    }


    setPointsIfDirty(points) {
        if (dirtyPoints(this.points, points))
            this.setPoints(points)
    }


    setProps(props) {
        if(this.id)
            this.tvShape().setProperties(props)
    }


    setPropsIfDirty(props) {
        // console.log('dirtyProps', this.props, props, dirtyProps(this.props, props))
        if( dirtyProps(this.props, props) )
            this.setProps(props)
    }


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


    delete() {
        // console.log('shape.delete', this.id)
        if (this.id === null) return
        this.lock++
        try {
            deleteShapeId(this.id)
            this.id = null
        } finally {
            this.lock--
        }
    }


    //
    // Model synchronization
    //

    onDirtyModel(toModel) {
        const old = {...this.model}
        toModel.call(this)
        const dirty = dirtyKeys(old, this.model)
        // console.log('onDirtyModel', old, this.model, dirty)
        if (dirty.length)
            this.onModel(this.model)
    }

    pointsFromModel() {return null}
    pointsToModel() {} // set the model using this.points
    propsFromModel() {return null}
    propsToModel() {} // set the model using this.props

    onModel(model) {} // called whenever points or props updates the model dictionary


    //
    // Overridable shape callbacks
    //

    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.
    onPoints(points) {} // the control points of an existing shape were changed
    onProps(props) {}  // the display properties of an existing shape were changed
    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 dirtyProps(propsA, propsB) {
    if (propsB===null)
        return propsA !== null
    const entries = Object.entries(propsB);
    if (propsA===null)
        return entries.length > 0
    for( const [k,v] of entries)
        if ( !(k in propsA) || propsA[k] !== v )
            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)]
    return [...(new Set(Object.keys(propsB)).union(new Set(Object.keys(propsA))))].filter((k)=> !(k in propsA) || propsA[k] !== propsB[k])
}


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) {
        if( this.shape.id )
            throw Error(`Created a shape ${shapeId} where one already existed ${this.shape.id}`)
        this.shape.id = shapeId
        if( this.shape.lock ) return
        this.creating = true
        invokeCallback(this.shape, 'onCreate', points, props)
    }

    onPoints(shapeId, _tvShape, points) {
        this.shape.points = points
        this.shape.onPoints(points)
        this.shape.onDirtyModel(this.shape.pointsToModel)
    }

    onProps(shapeId, _tvShape, props) {
        // console.log('onProps', props)
        if (this.creating) {
            this.creating = false
            return
        }
        this.shape.props = props
        this.shape.onProps(props)
        this.shape.onDirtyModel(this.shape.propsToModel)
    }

    onDraw() {
        if( this.shape.lock ) return
        invokeCallback(this.shape, 'onDraw')
    }

    onRedraw() {
        if( this.shape.lock ) return
        invokeCallback(this.shape, 'onRedraw')
    }

    onUndraw() {
        if( this.shape.lock ) return
        invokeCallback(this.shape, 'onUndraw')
    }

    onAddPoint() {
        if( this.shape.lock ) return
        invokeCallback(this.shape, 'onAddPoint')
    }

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

    onDrag(_shapeId, _tvShape, points) {
        if( this.shape.lock ) return
        invokeCallback(this.shape, 'onDrag', points)
    }

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

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

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

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


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

    pointsFromModel() {
        if (this.model.price === null) return null
        const time = this.points !== null && this.points.length > 0 ? this.points[0].time : 0
        return [{time:time, price:this.model.price}]
    }

    pointsToModel() {
        this.model.price = this.points[0].price
    }

    propsFromModel() {
        return this.model.color ? {linecolor: this.model.color} : null
    }

    propsToModel() {this.model.color=this.props.linecolor}

    onDrag(points) {
        const s = this.tvShape();
        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())
    }

}

