#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# DESCRIPTION: 2014 shift-right countdown (4-2-1-0)
#
# Run the script with
#   klayout -r path_to_script/2014.rb ...
# (it is important to specify a path, i.e. ".")
# or drag and drop the script to the KLayout window.

include RBA

if ! RBA.const_defined?("QDialog")
  raise "KLayout must be compiled with --with-qtbinding"
end

v = Application::instance.version.split(/\./)
3.times { |i| v[i] = (v[i] || "0").to_i }
if v[0] == 0 && (v[1] < 23 || (v[1] == 23 && v[2] < 1))
  raise "At least KLayout 0.23.1 required"
end

# The script which controls the show

Script = <<END

  a = trace(0, true)  # "4"
  after(a) { 
    at(time + 3) {
      a = trace(1, true)  # "2"
      after(a) {
        at(time + 3) {
          a = trace(2, true)  # "1"
          after(a) {
            at(time + 3) {
              a = trace(3, true)  # "0"
              after(a) {
                at(time + 4) { stop }
              }
            }
          }
        }
      }
    }
  }

  # Done

END

# The background structure which forms the basis of the net mesh

Polygons = [
 [ [245,15], [245,30], [215,30], [215,40], [245,40], [245,65], [240,65], [225,50], [225,40], 
   [215,40], [215,55], [245,85], [255,85], [255,40], [265,40], [265,30], [255,30], [255,15] ],
 [ [35,15], [35,40], [45,50], [70,50], [75,55], [75,70], [70,75], [50,75], [45,70], [45,65], 
   [35,65], [35,75], [45,85], [75,85], [85,75], [85,50], [75,40], [50,40], [45,35], [45,25], 
   [85,25], [85,15] ],
 [ [155,15], [155,25], [175,25], [175,65], [170,65], [160,55], [155,55], [155,65], [175,85], 
   [185,85], [185,25], [205,25], [205,15] ],
 [ [105,15], [95,25], [95,75], [105,85], [135,85], [145,75], [110,75], [105,70], [105,30], 
   [110,25], [130,25], [135,30], [135,70], [130,75], [145,75], [145,25], [135,15 ] ],
 [ [225,40], [225,50], [240,65], [245,65], [245,40 ] ],
 [ [110,25], [105,30], [105,70], [110,75], [130,75], [135,70], [135,30], [130,25 ] ],
 [ [0,0], [0,100], [300,100], [300,85], [45,85], [35,75], [35,65], [45,65], [45,70], 
   [50,75], [70,75], [75,70], [75,55], [70,50], [45,50], [35,40], [35,15], [85,15], 
   [85,25], [45,25], [45,35], [50,40], [75,40], [85,50], [85,75], [75,85], [105,85], 
   [95,75], [95,25], [105,15], [135,15], [145,25], [145,75], [135,85], [175,85], [155,65], 
   [155,55], [160,55], [170,65], [175,65], [175,25], [155,25], [155,15], [205,15], [205,25], 
   [185,25], [185,85], [245,85], [215,55], [215,30], [245,30], [245,15], [255,15], [255,30], 
   [265,30], [265,40], [255,40], [255,85], [300,85], [300,0 ] ],
]


# @brief A class representing a single spark
#
# This is scene object used by the ScriptPlayer to represent an
# object on the scene.
# A scene object must be able to produce itself on the layout.
# The object will receive a "move" call on each tick. When it decides
# it has to disappear it can unregister itself.
#
# A spark is a moving object which has a color (multiple colors
# can be specified and they are picked randomly each iteration).
# A spark can leave a trace, which means it will generate more
# sparks on it's way. After the specified burn time if over, a spark
# will remove itself.
#
# Sparks are subject to gravitation and hence accelerate towards
# negative y.

class Spark

  pts = [ [ -312, -440 ], [ -196, -32  ], [ -560, 180  ], [ -144, 176  ], [ -40,  620  ],
          [ 88,   184  ], [ 500,  244  ], [ 184,  -16  ], [ 356,  -424 ], [ 4,    -148 ] ]
  @@star = RBA::Polygon::new(pts.collect { |x,y| RBA::Point::new(x, y) })

  def initialize(script_runner, args)

    @script_runner = script_runner
    @script_runner.register(self)

    a = args[:a] || 0
    v = args[:v] || 0
    @vx = v * script_runner.tick_val * Math::sin(a / 180.0 * Math::PI)
    @vy = v * script_runner.tick_val * Math::cos(a / 180.0 * Math::PI)

    @x = args[:x] || x
    @y = args[:y] || y

    @layers = args[:c] || []  
    if !@layers.is_a?(Array)
      @layers = [ @layers ]
    end

    @burn_time = args[:t] || 0.2
    @trace = ((args[:trace] || 1) != 0)
    @g = args[:g] || 5
    @tg = args[:tg] || 5
    @stay = ((args[:stay] || 0) != 0)

  end

  def move

    if @burn_time > -0.001

      @x += @vx
      @y += @vy
      @vy -= @g * @script_runner.tick_val 

      @burn_time -= @script_runner.tick_val
      if @burn_time < 0.001
        @stay || @script_runner.unregister(self)
      elsif @trace
        @script_runner.register(Spark::new(@script_runner, 
                                           :x => @x, 
                                           :y => @y, 
                                           :t => 0.01 * rand(100), 
                                           :c => @layers, 
                                           :g => @tg, 
                                           :stay => @stay, 
                                           :trace => 0))
      end

    end

  end

  def produce(layout, top)
    f = 1.0 / layout.dbu
    r = rand
    l = @layers[(r * @layers.size).to_i]
    top.shapes(l).insert((@@star * (3 * r)).moved(f * @x, f * @y))
  end

end

# @brief A class representing a traced path
#
# This is also a scene object. The path tracer will receive a path
# (that is, a collection of Shape objects representing some net/path)
# and paint them by copying them piecewise to the canvas layer
#
# If final is true, it will add a bunch of sparks once it's done.
# 
# While it's painting it will produce some sparks at the position
# where the painting happens.

class PathTracer

  def initialize(script_runner, layer, shapes, spark_layers, final)
    @layer = layer
    @shapes = shapes
    @script_runner = script_runner
    @script_runner.register(self)
    @spark_layers = spark_layers
    @final = final
  end

  def move
    @shapes.empty? && @script_runner.unregister(self)
  end

  def produce(layout, top)
    p = @shapes[0] && @shapes[0].bbox.center
    4.times do 
      shape = @shapes.shift
      shape && top.shapes(@layer).insert(shape)
    end
    if !@shapes.empty?
      Spark::new(@script_runner, :x => p.x * layout.dbu, :y => p.y * layout.dbu, :c => @spark_layers, :t => 0.5, :trace => 0, :v => 20, :g => 5, :a => rand * 360)
    elsif @final
      30.times do 
        Spark::new(@script_runner, :x => p.x * layout.dbu, :y => p.y * layout.dbu, :c => @spark_layers, :t => 3, :trace => true, :v => 20 + rand * 50, :g => 0.5, :tg => 1, :a => rand * 360)
      end
    end
  end

end

# @brief The script player
#
# The script player has one basic method, namely "play" which 
# will receive the script and run it. The script is evaluated 
# in the context of the script player's instance and can
# register actions with "at" which itself call various methods,
# in particular "launch" which will launch a rocket.
#
# The script player manages a set of scene objects.
# Internally it keeps a timer that produces ticks in regular intervals.
# On each timer tick, it will look up the script whether new actions
# are to be executed and runs them.
# Next, it will call the "move" method for all objects registered.
# A scene object can use the opportunity to unregister itself
# if it is no longer required. The GC will then delete this object
# later.
# After all objects are moved, the objects which have not been 
# unregistered yet will be asked to produce themselves on the 
# layout which has been cleaned before.

class ScriptPlayer

  def initialize

    mw = Application.instance.main_window

    # Specify the tick value (the time between two steps on the scene)
    @tick = 0.05

    # create a new layout and configure the corresponding view properly
    # with black background and some features disabled
    # We need an editable layout (otherwise the shape object magic won't work).
    # "show_layout" is a trick to establish a layout in editable mode even if the 
    # program is not called in editable mode:
    @ly = RBA::Layout::new(true)
    mw.create_layout("", 0)
    @lv = mw.current_view
    @lv.show_layout(@ly, false)
    @lv.set_config("background-color", "#000000")
    @lv.set_config("bitmap-oversampling", "1")
    @lv.set_config("text-visible", "true")
    @lv.set_config("sel-transient-mode", "false")
    @lv.set_config("grid-visible", "false")
    @lv.set_config("drawing-workers", "0") # reduces flicker
    @lv.set_config("hide-empty-layers", "false") # saves time
    @lv.clear_layers

    # create a top cell
    @top = @ly.create_cell("TOP")
    @lv.select_cell(@top.cell_index, 0)
    
    # set up some layers to produce the net mesh on
    @lip = @ly.layer(1, 0)
    @lih = @ly.layer(10, 0)
    @lic = @ly.layer(11, 0)
    @liv = @ly.layer(12, 0)
    
    # set up a highlighted layer for producing the 
    # nets on by the PathTracer
    @lihh = @ly.layer(20, 0)

    # Stores the decoration layer (i.e. for sparks)
    @decoration_layers = [ ]
    
    # Set up layers for the sparks
    spark_colors = [ 0x603030, 0x705050, 0xe08000, 0xf0ff30, 0xffd000, 0x80f0ff, 0xf0f0ff ]
    @spark_layers = []

    spark_colors.each_with_index do |sc,i|
      l = @ly.layer(100 + i, 0)
      @decoration_layers << l
      @spark_layers << l
    end
    
    lp = RBA::LayerProperties::new
    lp.frame_color = lp.fill_color = 0x500000
    lp.dither_pattern = 4
    lp.source = "%" + @liv.to_s
    @lv.insert_layer(@lv.end_layers, lp)
    
    lp = RBA::LayerProperties::new
    lp.frame_color = lp.fill_color = 0x005000
    lp.dither_pattern = 3
    lp.source = "%" + @lic.to_s
    @lv.insert_layer(@lv.end_layers, lp)
    
    lp = RBA::LayerProperties::new
    lp.frame_color = lp.fill_color = 0x000050
    lp.dither_pattern = 8
    lp.source = "%" + @lih.to_s
    @lv.insert_layer(@lv.end_layers, lp)
    
    lp = RBA::LayerProperties::new
    lp.frame_color = lp.fill_color = 0xf0f0f0
    lp.dither_pattern = 8
    lp.source = "%" + @lihh.to_s
    @lv.insert_layer(@lv.end_layers, lp)
    
    spark_colors.each_with_index do |sc,i|
      lp = RBA::LayerProperties::new
      lp.frame_color = lp.fill_color = sc
      lp.dither_pattern = 0
      lp.source = "%" + @spark_layers[i].to_s
      @lv.insert_layer(@lv.end_layers, lp)
    end

    grid = 2.0
    unit = (1.0 / @ly.dbu + 0.5).to_i
    grid_unit = (grid / @ly.dbu + 0.5).to_i

    path_width = 0.4
    via_size = 0.3
    
    # Produce the polygons on the background layer
    # that way we can use the bounding box of the top cell
    Polygons.each do |p|
      poly = RBA::Polygon::new(p.collect { |pp| RBA::Point::new(pp[0] * unit, pp[1] * unit) })
      @top.shapes(@lip).insert(poly)
    end

    arena = @top.bbox
    ny = arena.height / grid_unit - 1
    nx = arena.width / grid_unit - 1
    
    # set up a bitmap which indicates all positions already occupied by a net
    bits = []
    nx.times { bits << [false] * ny }
    
    @paths = []
    
    # Produce nets inside each polygon
    Polygons.each do |p|

      # create the polygon
      poly = RBA::Polygon::new(p.collect { |pp| RBA::Point::new(pp[0] * unit, pp[1] * unit) })
      bbx = poly.bbox
    
      path = []
    
      # Determine a starting point
      p = nil
      poly.each_point_hull { |pt| p = pt; break }
      poly.move(-unit / 2, -unit / 2)
      
      # produce a net by performing a random walk inside the polygons
      # when caught in a dead end, use backtracking to find a way out until there is not escape
      x = (p.x + grid_unit - 1) / grid_unit
      y = (p.y + grid_unit - 1) / grid_unit
      n = nil
      bits[x][y] = true
      
      stack = [[x, y, n]]
      xdirs = [0, -1, 0, 1]
      ydirs = [1, 0, -1, 0]
      
      while true
        
        # pick a random direction in which we can proceed
        d = (rand * 4).to_i
        proceed = false
        xn = yn = nil
        4.times do |di|
          i = (d + di) % 4
          xn = x + xdirs[i]
          yn = y + ydirs[i]
          proceed = xn >= 0 && xn < nx && yn >= 0 && yn < ny && !bits[xn][yn] && 
                    poly.inside(RBA::Point::new(xn * grid_unit, yn * grid_unit))
          proceed && break
        end
        
        if proceed

          # proceed further: produce a net shape an continue
          px  = x * grid_unit;  py  = y * grid_unit
          pxn = xn * grid_unit; pyn = yn * grid_unit
          pts = [ RBA::Point::new(px, py), RBA::Point::new(pxn, pyn) ]
          nn = x == xn ? @liv : @lih
          w = (path_width * unit + 0.5).to_i
          path << @top.shapes(nn).insert(RBA::Path::new(pts, w, w / 2, w / 2))
          if nn != n
            d = (0.5 * via_size * unit + 0.5).to_i
            @top.shapes(@lic).insert(RBA::Box::new(px - d, py - d, px + d, py + d))
          end

          # go ahead
          x = xn; y = yn; n = nn

          # mark the new position as taken and remember it for backtracking
          bits[x][y] = true
          stack << [x, y, nn]

        elsif !stack.empty?

          # backtracking
          (x, y, n) = stack.pop

        else
          # no way out -> we're done
          break
        end
        
      end
      
      @paths << path
          
    end
    
    @lv.zoom_fit

    # Set up the timer
    @timer = QTimer.new
    @timer.interval = @tick * 1000
    @timer.singleShot = false
    @timer.timeout do 
      self.tick
    end

  end

  # Play the script: this method will evaluate the script in the context of 
  # "self" and start the timer
  def play(script)
    stop
    instance_eval(script)
    @time = 0.0
    @clear = true
    @timer.start 
  end

  # Stop the script execution
  def stop
    @clear = false
    @timer.stop
    @actions = { }
    @dep_actions = { } 
    @objects = { }
  end

  # Adds the given object to the scene
  def register(obj)
    @objects[obj.object_id] = obj
  end

  # Removes the given object from the scene
  def unregister(obj)
    @objects.delete(obj.object_id)
    # execute dependent actions
    (@dep_actions[obj.object_id] || []).each { |a| a.call }
  end

  # Gets the tick value (the time between two calls of "move" or "produce")
  # in seconds
  def tick_val
    @tick
  end

  # Gets the current time
  def time
    @time
  end

  # Trace a path with index i
  def trace(i, final = false)
    PathTracer::new(self, @lihh, @paths[i], @spark_layers, final)
  end

  # Register an action at the given time
  def at(time, &action)
    @actions[(time / @tick + 0.5).to_i] ||= []
    @actions[(time / @tick + 0.5).to_i].push(action)
  end

  # Register an action after the given object has finished
  def after(object, &action)
    @dep_actions[object.object_id] ||= []
    @dep_actions[object.object_id].push(action)
  end

  # Timer callback: perform the next action
  def tick

    # stop if the view was closed
    if @lv.destroyed?
      stop
      return
    end

    # clear selection and transactions which may depend
    # on any object we will delete now
    @lv.cancel
    @lv.clear_transactions

    # perform the actions for the current time slot
    @time += @tick
    actions = @actions[(@time / @tick + 0.5).to_i]
    if actions
      actions.each do |a|
        a.call
      end
    end

    # clear all layers except the mask (text) layer
    if @clear
      @decoration_layers.each do |n|
        @top.shapes(n).clear
      end
    end

    # call "move" on each object - since "move" may unregister the 
    # object we cannot iterate over the hash directly.
    objs =  @objects.collect { |id,obj| obj }
    objs.each do |obj|
      obj.move
    end
 
    # call produce on the objects
    objs =  @objects.collect { |id,obj| obj }
    objs.each do |obj|
      obj.produce(@ly, @top)
    end

  end

end

# The main action of this module: instantiate the script player
# and initiate the playing of the script
$y2014script = ScriptPlayer.new
$y2014script.play(Script)

# After the player has been started, run the real application
# (this will do nothing if the script has been started by dragging
# it into the already running application)
Application.instance.exec


