#import "@preview/zap:0.4.0": * // TODO: Using wrong version, because 5.0.0 breaks custom stuff
#import "@preview/cetz:0.4.2": draw
#import cetz.draw: *

#let norator(name, node, scale: 1.0, ..params) = {
  let custom-style = (
    width: 1.41 * scale,
    height: 1.41 * scale
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: position.len() < 2)

    merge-path(close: true, {
      arc((-custom-style.width / 2, 0), radius: custom-style.height / 5, start: 45deg, stop: 315deg, anchor: "arc-center", name: "right-arc")
      arc((custom-style.width / 2, 0), radius: custom-style.height / 5, start: 135deg, stop: -135deg, anchor: "arc-center", name: "left-arc")
    })
  }

  component("norator", name, node, draw: draw, ..params)
}

#let nullator(name, node, scale: 1.0, ..params) = {
  let custom-style = (
    width: 1.41 * scale,
    height: 1.41 * scale
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: position.len() < 2)

    circle((0, 0), radius: (custom-style.width / 2, custom-style.height / 6))
  }

  component("nullator", name, node, draw: draw, ..params)
}

#let diode(name, node, type: none, ..params) = {
  assert((type in ("ideal", "convex", "concave", "zener", "real") or type == none), message: "type must be ideal, convex, concave, zener, ...")
  if type == none {
    type = "ideal"
  }


  let custom-style = (
    radius: 0.3,
    width: 0.28,
  )

  let draw(ctx, position, style) = {
    translate((-custom-style.radius / 4, 0))
    interface((-custom-style.radius / 2, -custom-style.radius), (custom-style.radius, custom-style.radius), io: position.len() < 2)

    polygon((0, 0), 3, radius: custom-style.radius, fill: if type in ("real", "zener") { black } else { auto })

    if type == "zener" {
      merge-path({
        line((custom-style.radius, custom-style.width), (custom-style.radius, -custom-style.width))
        line((custom-style.radius, custom-style.width), (custom-style.radius + custom-style.width / 3, custom-style.width))
      })
    } else { // ideal, convex, concave
      line((custom-style.radius, custom-style.width), (custom-style.radius, -custom-style.width))
    }

    if type == "concave" {
      arc((-custom-style.radius/10,0), radius: (custom-style.radius / 4, custom-style.radius / 3), start: 90deg, stop: -90deg, anchor: "chord-center")
    } else if type == "convex" {
      circle((0, 0), radius: custom-style.radius / 3.5)
    }
  }

  component("custom-diode", name, node, draw: draw, ..params)
}

#let inductor(name, node, ..params) = {
  let custom-style = (
    width: 1,
    height: 0.4
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: position.len() < 2)

    cetz.decorations.coil(line((-custom-style.width / 2, 0), (custom-style.width / 2, 0)), amplitude: custom-style.width / 3, segments: 5)
  }

  component("indcutor", name, node, draw: draw, ..params)
}

#let eintor(name, node, ..params) = {
  let custom-style = (
    width: 1,
    height: 0.4
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: position.len() < 2)

    rect((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), ..style)
    rect((custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 4, custom-style.height / 2), fill: black, ..style)
  }

  component("eintor", name, node, draw: draw, ..params)
}

#let controlled-source(name, node, scale: 1.0, type: none, ..params) = {
  assert((type in ("voltage", "current")), message: "type must be voltage or current, ...")

  let custom-style = (
    radius: 0.3 * scale,
    width: 0.28 * scale,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.radius, -custom-style.radius), (custom-style.radius, custom-style.radius), io: position.len() < 2)

    polygon((0, 0), 4, radius: custom-style.radius)

    if type == "voltage" {
      line((-custom-style.radius, 0), (custom-style.radius, 0))
    } else {
      line((0, -custom-style.radius), (0, custom-style.radius))
    }

    anchor("north", (0, custom-style.radius))
    anchor("south", (0, -custom-style.radius))
  }

  component("custom-diode", name, node, draw: draw, ..params)
}

#let empty(name, node, ..params) = {
  let draw(ctx, position, style) = {
    interface((-0.5, -0.2), (0.5, 0.2), io: true)
  }

  component("empty", name, node, draw: draw, ..params)
}

#let blackbox(name, node, mirror: false, label: "", ..params) = {
  assert(params.pos().len() == 0, message: "blackbox only supports one node")

  let custom-style = (
    width: 1,
    height: 1,
    length: 0.2,
  )

  let factor = if mirror { -1 } else { 1 }

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: false)

    rect((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), ..style)
    content((0, 0), label)

    anchor("in", (factor * (custom-style.width / 2 + custom-style.length), custom-style.height / 3))
    anchor("out", (factor * (custom-style.width / 2 + custom-style.length), -custom-style.height / 3))

    wire("in", (rel: (-factor * custom-style.length, 0)))
    wire("out", (rel: (-factor * custom-style.length, 0)))
  }

  component("blackbox", name, node, draw: draw, ..params)
}

#let zweitor(name, node, label: "", ..params) = {
  assert(params.pos().len() == 0, message: "zweitor only supports one node")
  // assert(type(label) == content, message: "label has to be content")

  let custom-style = (
    width: 1,
    height: 1,
    length: 0.2,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: false)

    rect((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), ..style)
    content((0, 0), label)

    anchor("11", (-custom-style.width / 2 - custom-style.length, custom-style.height / 3))
    anchor("12", (-custom-style.width / 2 - custom-style.length, -custom-style.height / 3))
    anchor("21", (custom-style.width / 2 + custom-style.length, custom-style.height / 3))
    anchor("22", (custom-style.width / 2 + custom-style.length, -custom-style.height / 3))

    wire("11", (rel: (custom-style.length, 0)))
    wire("12", (rel: (custom-style.length, 0)))
    wire("21", (rel: (-custom-style.length, 0)))
    wire("22", (rel: (-custom-style.length, 0)))
  }

  component("zweitor", name, node, draw: draw, ..params)
}

#let gyrator(name, node, scale: 1.0, constant: $R_d$, invert: false, ..params) = {
  assert(params.pos().len() == 0, message: "gyrator only supports one node")

  let custom-style = (
    width: 5 * scale,
    height: 5 * scale,
    length: 0.5 * scale,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: true)

    anchor("11", (-custom-style.width / 2 - custom-style.length, custom-style.height / 3))
    anchor("12", (-custom-style.width / 2 - custom-style.length, -custom-style.height / 3))
    anchor("21", (custom-style.width / 2 + custom-style.length, custom-style.height / 3))
    anchor("22", (custom-style.width / 2 + custom-style.length, -custom-style.height / 3))

    wire("11", (rel: (custom-style.length + custom-style.width / 3, 0)), (to: "12", rel: (custom-style.length + custom-style.width / 3, 0)), "12")
    wire("21", (rel: (-custom-style.length - custom-style.width / 3, 0)), (to: "22", rel: (-custom-style.length - custom-style.width / 3, 0)), "22")

    arc((-custom-style.width / 6, 0), radius: custom-style.width / 9, start: 90deg, stop: -90deg, anchor: "chord-center")
    arc((custom-style.width / 6, 0), radius: custom-style.width / 9, start: 90deg, stop: 270deg, anchor: "chord-center")

    line(name: "dual", (to: "11", rel: (custom-style.length + custom-style.width / 3, 0.1)), (to: "21", rel: (-custom-style.length - custom-style.width / 3, 0.1)), mark: (end: if not invert { ">" }, start: if invert { ">" }, scale: 0.7), stroke: 0.5pt, fill: black)
    content((to: "dual.centroid", rel: (0, 0.1)), constant, anchor: "south")
  }

  component("gyrator", name, node, draw: draw, ..params)
}

#let nullor(name, node, scale: 1.0, ..params) = {
  assert(params.pos().len() == 0, message: "nullor only supports one node")

  let custom-style = (
    width: 5 * scale,
    height: 5 * scale,
    length: 0.5 * scale,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: true)

    anchor("11", (-custom-style.width / 2 - custom-style.length, custom-style.height / 3))
    anchor("12", (-custom-style.width / 2 - custom-style.length, -custom-style.height / 3))
    anchor("21", (custom-style.width / 2 + custom-style.length, custom-style.height / 3))
    anchor("22", (custom-style.width / 2 + custom-style.length, -custom-style.height / 3))

    nullator("N1", scale: scale, (to: "11", rel: (custom-style.length + custom-style.width / 3, 0)), (to: "12", rel: (custom-style.length + custom-style.width / 3, 0)))
    wire("11", "N1.in")
    wire("12", "N1.out")

    norator("N2", scale: scale, (to: "21", rel: (-custom-style.length - custom-style.width / 3, 0)), (to: "22", rel: (-custom-style.length - custom-style.width / 3, 0)))
    wire("21", (rel: (-custom-style.length - custom-style.width / 3, 0)))
    wire("22", (rel: (-custom-style.length - custom-style.width / 3, 0)))
  }

  component("nullor", name, node, draw: draw, ..params)
}

#let transformer(name, node, scale: 1.0, ..params) = {
  assert(params.pos().len() == 0, message: "transformer only supports one node")

  let custom-style = (
    width: 5 * scale,
    height: 5 * scale,
    length: 0,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: true)

    anchor("11", (-custom-style.width / 2 - custom-style.length, custom-style.height / 3))
    anchor("12", (-custom-style.width / 2 - custom-style.length, -custom-style.height / 3))
    anchor("21", (custom-style.width / 2 + custom-style.length, custom-style.height / 3))
    anchor("22", (custom-style.width / 2 + custom-style.length, -custom-style.height / 3))

    anchor("C1in", (to: "11", rel: (custom-style.length + custom-style.width / 3, -custom-style.height / 6)))
    anchor("C1out", (to: "12", rel: (custom-style.length + custom-style.width / 3, custom-style.height / 6)))
    cetz.decorations.coil(line("C1in", "C1out"), amplitude: custom-style.width / 10, segments: 5)
    zwire("11", "C1in", ratio: 100%)
    zwire("12", "C1out", ratio: 100%)

    anchor("C2in", (to: "21", rel: (-custom-style.length - custom-style.width / 3, -custom-style.height / 6)))
    anchor("C2out", (to: "22", rel: (-custom-style.length - custom-style.width / 3, custom-style.height / 6)))
    cetz.decorations.coil(line("C2out", "C2in"), amplitude: custom-style.width / 10, segments: 5)
    zwire("21", "C2in", ratio: 100%)
    zwire("22", "C2out", ratio: 100%)

    line((custom-style.width * 0.025, -custom-style.height / 5), (custom-style.width * 0.025, custom-style.height / 5))
    line((-custom-style.width * 0.025, -custom-style.height / 5), (-custom-style.width * 0.025, custom-style.height / 5))

    content((0, custom-style.height / 3), anchor: "south", text(size: 0.75em)[$ü : 1$])
  }

  component("transformer", name, node, draw: draw, ..params)
}

#let controlled-source(name, node, scale: 1.0, left: "VC", right: "VS", i: none, ..params) = {
  assert(params.pos().len() == 0, message: "controlled source only supports one node")

  let custom-style = (
    width: 5 * scale,
    height: 5 * scale,
    length: 0.5 * scale,
  )

  let draw(ctx, position, style) = {
    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: true)

    anchor("11", (-custom-style.width / 2 - custom-style.length, custom-style.height / 3))
    anchor("12", (-custom-style.width / 2 - custom-style.length, -custom-style.height / 3))
    anchor("21", (custom-style.width / 2 + custom-style.length, custom-style.height / 3))
    anchor("22", (custom-style.width / 2 + custom-style.length, -custom-style.height / 3))

    if left == "VC" {
      wire("11", (rel: (custom-style.length + custom-style.width / 3, 0)))
      wire("12", (to: "12", rel: (custom-style.length + custom-style.width / 3, 0)))
    } else if left == "CS" {
      wire("11", (rel: (custom-style.length + custom-style.width / 3, 0)), (to: "12", rel: (custom-style.length + custom-style.width / 3, 0)), "12")
    } else {
      assert(false, message: "unknown value for left")
    }

    if right == "CS" {
      // isource("source", (rel: (-custom-style.length - custom-style.width / 3, 0), to: "21"), (rel: (-custom-style.length - custom-style.width / 3, 0), to: "22"), scale: scale, dependent: true, i: i)

      anchor("sp", (rel: (-custom-style.length - custom-style.width / 3, 0), to: "21"))
      anchor("sn", (rel: (-custom-style.length - custom-style.width / 3, 0), to: "22"))
    } else {
      assert(false, message: "unknown value for right")
    }

    wire("21", "sp")
    wire("22", "sn")

    // arc((-custom-style.width / 6, 0), radius: custom-style.width / 9, start: 90deg, stop: -90deg, anchor: "chord-center")
    // arc((custom-style.width / 6, 0), radius: custom-style.width / 9, start: 90deg, stop: 270deg, anchor: "chord-center")
  }

  component("gyrator", name, node, draw: draw, ..params)
}

#let ideal-opamp(name, node, invert: false, scale: 1.0, angle: 0deg, ..params) = {
  assert(params.pos().len() == 0, message: "opamp supports only one node")

  let custom-style = (
    width: 1.8 * scale,
    height: 1.75 * scale,
    padding: .28 * scale,
    sign-stroke: .55pt,
    sign-size: .14 * scale,
    sign-delta: .45 * scale,
  )

  let draw(ctx, position, style) = {
    rotate(angle)

    interface((-custom-style.width / 2, -custom-style.height / 2), (custom-style.width / 2, custom-style.height / 2), io: true)

    let sgn = if invert { -1 } else { 1 }
    anchor("minus", (-custom-style.width / 2, sgn * custom-style.sign-delta))
    anchor("plus", (-custom-style.width / 2, -sgn * custom-style.sign-delta))

    scope({
      translate((-custom-style.width / 6, 0))
      polygon((0, 0), 3, radius: custom-style.width * 2 / 3, name: "poly")
    })

    // This is horrendous, don't look at it
    anchor("ground", ((rel: (0deg, custom-style.width * 2 / 3), to: "poly.default"), 50%, (rel: (-120deg, custom-style.width * 2 / 3), to: "poly.default")))
    anchor("vcc", ((rel: (0deg, custom-style.width * 2 / 3), to: "poly.default"), 50%, (rel: (120deg, custom-style.width * 2 / 3), to: "poly.default")))

    set-style(stroke: custom-style.sign-stroke)
    line((rel: (custom-style.padding - custom-style.sign-size, 0), to: "minus"), (rel: (2 * custom-style.sign-size, 0)))
    line((rel: (custom-style.padding - custom-style.sign-size, 0), to: "plus"), (rel: (2 * custom-style.sign-size, 0)))
    line((rel: (custom-style.padding, -custom-style.sign-size), to: "plus"), (rel: (0, 2 * custom-style.sign-size)))

    content((custom-style.width / 2, 0), text(size: 12pt * scale)[$ infinity $], anchor: "east", padding: .45 * scale, angle: angle)
  }

  component("ideal-opamp", name, node, draw: draw, ..params)
}

#let ground(name, node, ..params) = {
  assert(params.pos().len() == 0, message: "ground supports only one node")

  let custom-style = (
    width: 0.46,
    distance: 0.28,
  )

  let draw(ctx, position, style) = {
    wire((0, 0), (0, -custom-style.distance))
    let delta = custom-style.width / 2

    line((-custom-style.width / 2, -custom-style.distance), (custom-style.width / 2, -custom-style.distance))

    interface((-custom-style.width / 2, custom-style.distance), (custom-style.width / 2, -custom-style.distance))
    anchor("default", (0, 0))
  }

  component("ground", name, node, draw: draw, ..params)
}

#let offset-line(from, to, dist, func) = {
  import cetz: vector

  get-ctx(ctx => {
    let (_, from, to) = cetz.coordinate.resolve(ctx, from, to)

    let line = vector.sub(to, from)

    let normal = (-line.at(1), line.at(0))

    let offset = vector.scale(vector.norm(normal), dist)

    let c = vector.add(from, offset)
    let d = vector.add(c, line)

    func(c, d)
  })
}

#let voltage(from, to, distance: 0, anchor: "east", label) = {
  offset-line(from, to, distance, (from, to) => {
    line((from, 15%, to), (from, 85%, to), stroke: 0.5pt, mark: (end: (symbol: ">", scale: 0.5, fill: black)), name: "V1")
    content("V1.centroid", padding: 0.1, anchor: anchor, label)
  })
}

#let element-voltage(from, to, distance: 0.5, anchor: "east", label) = {
  voltage(from, to, anchor: anchor, distance: if anchor in ("east", "north") { -distance } else { distance }, label)
}


#let labeledNode(s, center, color: rgb("#00000000"), scale_t: 1, ..style) = {
  
  (
    ctx => {
      // Define a default style
      let def-style = (n: 5, inner-radius: .5, radius: 1, stroke: auto, fill: auto)
      
      // Resolve center to a vector
      let (ctx, center) = cetz.coordinate.resolve(ctx, center)
      
      // Resolve the current style ("star")
      let style = cetz.styles.resolve(ctx.style, merge: style.named(), base: def-style, root: "star")
      
      let paths = (
        cetz.drawable.ellipse(center.at(0), center.at(1), 0, scale_t*0.15, scale_t*0.15, fill: color, stroke: (paint: black, thickness: scale_t
        *0.25mm)),
        cetz.drawable.content((center.at(0),center.at(1)+0.03), 1, 1, none, text(size: 7pt * scale_t, s)),
      )
      (
        ctx: ctx, 
        drawables: cetz.drawable.apply-transform(ctx.transform, paths)
      )
    },
  )
}