module Extractor
    open CommonTypes
    open Fable.Core


    /// access javascript reference equality
    [<Emit("($1 === $0)")>]
    let referenceEquality (a1: obj) (a2: obj) : bool = jsNative

    let sortQBy (byFun: 'a -> 'b) (ids: 'a list) =
        let mutable isSorted = true

        for i = 0 to ids.Length - 2 do
            if byFun (ids[i]) > byFun (ids[i + 1]) then
                isSorted <- false

        if isSorted then ids else List.sortBy byFun ids

    /// Formats a ConnectionId for logging purposes
    let logConnId (id: ConnectionId) =
        id
        |> (fun (ConnectionId str) -> str)
        |> (fun str -> str[0..2])

    /// Transform the CanvasState into an f# data structure, with layout data removed (for checking electrically significant changes).
    /// Components and connections are sorted to make them order-invariant - selecting components alters order.
    let extractReducedState (state: CanvasState) : ReducedCanvasState =
        let (components: Component list), (connections: Connection list) = state

        let comps =
            components
            |> List.map (fun comp ->
                if
                    comp.SymbolInfo = None
                    && comp.H = 0.
                    && comp.W = 0.
                    && comp.X = 0.
                    && comp.Y = 0.
                then
                    comp
                else
                    { comp with H = 0.; W = 0.; X = 0.; Y = 0.; SymbolInfo = None })
            |> sortQBy (fun comp -> comp.Id)

        let conns =
            connections
            |> List.map (fun conn ->
                if conn.Vertices = [] then
                    conn
                else
                    { conn with Vertices = [] })
            |> sortQBy (fun conn -> conn.Id)

        ReducedCanvasState(comps, conns)

    let inline connsAreEqual (conn1: Connection) (conn2: Connection) =
        let portsEqual (p1: Port) (p2: Port) =
            p1.Id = p2.Id
            && p1.HostId = p2.HostId
            && p1.PortNumber = p2.PortNumber
            && p1.PortType = p2.PortType

        portsEqual conn1.Source conn2.Source
        && portsEqual conn1.Target conn2.Target

    let inline compsAreEqual (comp1: Component) (comp2: Component) =
        comp1.Id = comp2.Id
        && comp1.InputPorts = comp2.InputPorts
        && comp1.OutputPorts = comp2.OutputPorts
        && comp1.Type = comp2.Type

    let inline compsAreEqualExInputDefault (comp1: Component) (comp2: Component) =
        match comp1.Type, comp2.Type with
        | Input1 _, Input1 _ ->
            comp1.Id = comp2.Id
            && comp1.InputPorts = comp2.InputPorts
            && comp1.OutputPorts = comp2.OutputPorts
        | _ ->
            comp1.Id = comp2.Id
            && comp1.InputPorts = comp2.InputPorts
            && comp1.OutputPorts = comp2.OutputPorts
            && comp1.Type = comp2.Type

    /// Is circuit (not geometry) the same for two CanvasStates? fast comparison
    let stateIsEqual (cs1: CanvasState) (cs2: CanvasState) =
        let comps1, conns1 = cs1
        let comps2, conns2 = cs2

        comps1.Length = comps2.Length
        && conns1.Length = conns2.Length
        && List.forall2 compsAreEqual comps1 comps2
        && List.forall2 connsAreEqual conns1 conns2

    let stateIsEqualExInputDefault (cs1: CanvasState) (cs2: CanvasState) =
        let comps1, conns1 = cs1
        let comps2, conns2 = cs2

        comps1.Length = comps2.Length
        && conns1.Length = conns2.Length
        && List.forall2 compsAreEqualExInputDefault comps1 comps2
        && List.forall2 connsAreEqual conns1 conns2

    /// Are two lists of vertices are very similar.
    let verticesAreSame (fixedOffset: XYPos) tolerance (conns1: (float * float * bool) list) (conns2: (float * float * bool) list) =
        // Define a helper function to calculate the squared distance
        let sq x = x * x

        // Define a helper function to handle tolerance checking
        let diff m1 m2 = if m1 <> m2 then tolerance else 0.

        // Check if the lengths of the lists are equal
        if List.length conns1 <> List.length conns2 then
            false
        else
            // Initialize a mutable variable to accumulate errors
            let mutable errSum = 0.0

            // Calculate the error sum using iter2 (safe to use after length check)
            (conns1, conns2)
            ||> List.iter2 (fun (x1, y1, m1) (x2, y2, m2) ->
                errSum <- errSum + sq (x1 - x2 - fixedOffset.X) + sq (y1 - y2 - fixedOffset.Y) + diff m1 m2
            )

            // Return true if the total error is less than the given tolerance
            errSum < tolerance // REVIEW - check if the formatted code is the same as the original F# source code as shown below

    // let verticesAreSame tolerance (conns1:(float*float*bool) list) (conns2: (float*float*bool) list) =
    //     let diff m1 m2 = if m1 <> m2 then tolerance else 0.
    //     let sq x = x*x
    //     conns1.Length = conns2.Length &&
    //     let mutable errSum = 0.
    //     (conns1,conns2)
    //     ||> List.iter2 (fun (x1,y1,m1) (x2,y2,m2) -> errSum <- errSum + sq(x1-x2) + sq(y1-y2) + diff m1 m2)
    //     errSum < tolerance

    /// evil mutable for debugging only
    /// allows easy access to specific connections - so it can be highlighted in GUI
    let mutable debugChangedConnections: ConnectionId list = []

    let printConnErrors (connFixedOffset: XYPos)=
        List.mapi (fun i (c1,c2) ->
            let lengthDiff = c1.Vertices.Length - c2.Vertices.Length
            if c1.Id <> c2.Id then
                printfn "*** Connection Ids don't match"
            if lengthDiff <> 0 then
                printf "%s" $"Conn {i} {logConnId (ConnectionId c1.Id)}: Length diff: {lengthDiff}"
            else
                printf "%s" $"Conn {i} {logConnId (ConnectionId c1.Id)}: \
                    Vertices don't match: fixedoffset = %.1f{connFixedOffset.X},%.1f{connFixedOffset.Y}"
                printfn "Vertex deltas: %A" ((c1.Vertices,c2.Vertices) ||> List.map2 (fun (x1,y1,_) (x2,y2,_) -> (x1-x2),(y1-y2)))
            c1,c2)

    /// Are two lists of connections identical
    let compareConns tolerance conns1 conns2 =
        let connIdA (conns: Connection List) = conns |> sortQBy (fun conn -> conn.Id)
        let connsA1 = connIdA conns1
        let connsA2 = connIdA conns2
        /// if whole ckt has been translated this will be the offset.
        /// This offset for all vertices => connections still the same.
        let connFixedOffset =
            let xy (x,y,_) = {X=x;Y=y}
            match connsA1,connsA2 with
            | c1::_, c2::_ ->
                xy c1.Vertices[0] - xy c2.Vertices[0]
            | _ ->
                {X=0.; Y=0.}
        match connsA1, connsA2 with
        | a,b when a.Length <> b.Length ->
            //printfn "Connection list lengths don't match"
            false
        | a,b when not <| List.forall2 (fun c1 c2 -> verticesAreSame connFixedOffset tolerance c1.Vertices c2.Vertices) a b ->
            List.zip a b
            |> List.filter (fun (c1,c2) -> not <| verticesAreSame connFixedOffset tolerance c1.Vertices c2.Vertices)
            //|> printConnErrors connFixedOffset
            |> List.map fst
            |> List.map (fun (badConn: Connection) -> ConnectionId badConn.Id)
            |> (fun lst ->
                //printfn "%d bad connections" lst.Length
                debugChangedConnections <- lst)
            false
        | _ -> true
            


    /// Are two lists of components identical
    let compareComps tolerance comps1 comps2 =
        let isClose a b = float ((a - b) * (a - b)) < tolerance
        let compIdA (comps: Component List) = comps |> sortQBy (fun comp -> comp.Id)
        let compsA1 = compIdA comps1
        let compsA2 = compIdA comps2

        compsA1.Length = compsA2.Length
        && List.forall2 (fun (c1: Component) (c2: Component) -> isClose c1.X c2.X && isClose c1.Y c2.Y) compsA1 compsA2

    /// Robust comparison of two schematics. Tolerance determines how similar
    /// counts as equal.
    /// cannot use equality because float vertices may not be identical
    /// use to detemine whether schematic needs to be saved
    /// NB for electrical circuit comparison use extractReducedState.
    let compareCanvas (tolerance: float) ((comps1, conns1): CanvasState) ((comps2, conns2): CanvasState) =
        let reduce (comps: Component list) = comps |> sortQBy (fun comp -> comp.Id)
        let compsOk = reduce comps1 = reduce comps2
        let compsSamePos = compareComps tolerance comps1 comps2
        let connsOk = compareConns tolerance conns1 conns2
        let comparesEqual = compsOk && compsSamePos && connsOk
        //if not comparesEqual then
            //printf "%s" $"comps:{compsOk}, compsSamePos:{compsSamePos}, connsOk:{connsOk}"
        comparesEqual
        

    /// Compare the name and IOs of two sheets as loadedcomponents
    /// For backups, if these chnage something major has happened
    let compareIOs (ldc1: LoadedComponent) (ldc2: LoadedComponent) =
        Set(ldc1.InputLabels) = Set(ldc2.InputLabels)
        && ldc1.Name = ldc2.Name

    /// Is circuit (not geometry) the same for two LoadedComponents? They must also have the same name
    let loadedComponentIsEqual (ldc1: LoadedComponent) (ldc2: LoadedComponent) =
        ldc1.InputLabels = ldc2.InputLabels
        && ldc1.OutputLabels = ldc2.OutputLabels
        && stateIsEqual ldc1.CanvasState ldc2.CanvasState
        && ldc1.Name = ldc2.Name

    /// Is circuit (not geometry) the same for two LoadedComponents? They must also have the same name
    let loadedComponentIsEqualExInputDefault (ldc1: LoadedComponent) (ldc2: LoadedComponent) =
        ldc1.InputLabels = ldc2.InputLabels
        && ldc1.OutputLabels = ldc2.OutputLabels
        && stateIsEqualExInputDefault ldc1.CanvasState ldc2.CanvasState
        && ldc1.Name = ldc2.Name

    /// get sheet I/O labels in correct order based on position of components
    let getOrderedCompLabels compType ((comps, _): CanvasState) =
        comps
        |> List.collect (fun comp ->
            let sortKey = comp.Y, comp.X

            match comp.Type, compType with
            | Input1(n, defaultVal), Input1 _ -> [ sortKey, (comp.Label, n) ]
            | Output n, Output _ -> [ sortKey, (comp.Label, n) ]
            | _ -> [])
        |> List.sortBy fst
        |> List.map snd

    /// Extract the labels and bus widths of the inputs and outputs nodes as a signature.
    /// Form is inputs,outputs
    let parseDiagramSignature canvasState : (string * int) list * (string * int) list =
        let inputs = getOrderedCompLabels (Input1(0, None)) canvasState
        let outputs = getOrderedCompLabels (Output 0) canvasState
        inputs, outputs

    /// extract the fields compared to check circuit equality
    let extractLoadedSimulatorComponent (canvas: CanvasState) (name: string) =
        let inputs, outputs = parseDiagramSignature canvas
        //printfn "parsed component"
        let ldc =
            { Name = name
              TimeStamp = System.DateTime.Now
              WaveInfo = None
              FilePath = ""
              CanvasState = canvas
              InputLabels = inputs
              OutputLabels = outputs
              Form = None
              Description = None }

        ldc

    /// Returns true if project exists and ldc is electrically identical to same sheet in project
    /// canvasState must be the project currently open state.
    let loadedComponentIsSameAsProject (canvasState: CanvasState) (ldc: LoadedComponent) (p: Project option) =
        let ldcIsEq ldc1 ldc2 =
            ldc1.InputLabels = ldc2.InputLabels
            && ldc1.OutputLabels = ldc2.OutputLabels
            && stateIsEqual ldc1.CanvasState ldc2.CanvasState

        match ldc.Name, p with
        | "", _
        | _, None -> false
        | name, Some p when name = p.OpenFileName ->
            let ins, outs = parseDiagramSignature canvasState
            let sort = List.sort

            stateIsEqual canvasState ldc.CanvasState
            && sort ins = sort ldc.InputLabels
            && sort outs = sort ldc.OutputLabels
        | name, Some p ->
            List.tryFind (fun ldc -> ldc.Name = name) p.LoadedComponents
            |> Option.map (fun ldc' -> ldcIsEq ldc' ldc)
            |> Option.defaultValue false

    /// add given name,state to loadedcomponent lits as a loaded component (overwriting existing if needed)
    let addStateToLoadedComponents openFileName canvasState loadedComponents =
        let ins, outs = parseDiagramSignature canvasState

        let ldc: LoadedComponent =
            { Name = openFileName
              InputLabels = ins
              OutputLabels = outs
              CanvasState = canvasState
              Form = None
              Description = None
              WaveInfo = None
              FilePath = ""
              TimeStamp = System.DateTime.Now }

        loadedComponents
        |> List.filter (fun ldc -> ldc.Name <> openFileName)
        |> (fun ldcs -> ldc :: ldcs)

    /// the inverse of addStateToLoadedConponents
    /// The loadedComponent list does NOT include diagramName
    let getStateAndDependencies (diagramName: string) (ldcs: LoadedComponent list) =
        ldcs
        |> List.tryFind (fun ldc -> ldc.Name = diagramName)
        |> Option.map (fun ldc -> ldc.CanvasState)
        |> Option.map (fun cs -> diagramName, cs, List.filter (fun ldc -> ldc.Name <> diagramName) ldcs)
        |> Option.defaultWith (fun () -> failwithf $"Error - can't find {diagramName} in dependencies")
