require_relative "shake_grammar/lexer"
require_relative "shake_grammar/catcher"

class Tracksperanto::Import::ShakeScript < Tracksperanto::Import::Base
  
  def self.human_name
    "Shake .shk script file"
  end
  
  def self.distinct_file_ext
    ".shk"
  end
  
  def self.known_snags
    'Expressions in node parameters may cause parse errors or incomplete imports. ' +
    'Take care to remove expressions or nodes containing them first.'
  end
  
  def each(&blk)
    s = Sentinel.new
    s.progress_proc = method(:report_progress)
    s.tracker_proc = blk.to_proc
    TrackExtractor.new(@io, s)
  end
  
  private
  
  #:nodoc:
  
  class Sentinel
    attr_accessor :start_frame, :tracker_proc, :progress_proc
    def start_frame
      @start_frame.to_i
    end
  end
  
  # Extractor. Here we define copies of Shake's standard node creation functions.
  class TrackExtractor < Tracksperanto::ShakeGrammar::Catcher
    include Tracksperanto::ZipTuples
    
    # SetTimeRange("-5-15") // sets time range of comp
    # We use it to avoid producing keyframes which start at negative frames
    def settimerange(str)
      sentinel.start_frame = str.to_i if str.to_i < 0
    end
    
    # Normally, we wouldn't need to look for the variable name from inside of the funcall. However,
    # in this case we DO want to take this shortcut so we know how the tracker node is called
    def push(atom)
      return super unless atom_is_tracker_assignment?(atom)
      
      node_name = atom[1][-1]
      trackers = atom[2][1][1..-1]
      trackers.map do | tracker |
        tracker.name = [node_name, tracker.name].join("_")
        # THIS IS THE MOST IMPORTANT THINGO
        sentinel.tracker_proc.call(tracker)
      end
    end
    
    # Find whether the passed atom includes a [:trk] on any level
    def deep_include?(array_or_element, atom_name)
      return false unless array_or_element.is_a?(Array)
      return true if array_or_element[0] == atom_name
      
      array_or_element.each do | elem |
         return true if deep_include?(elem, atom_name)
      end
      
      false
    end
    
    # An atom that is a tracker node will look like this
    # [:assign, [:vardef, "Stabilize2"], [:retval, [:trk, <T "track1" with 116 keyframes>, <T "track2" with 116 keyframes>, <T "track3" with 116 keyframes>, <T "track4" with 89 keyframes>]]]
    # a Stabilize though will look like this
    # [:assign, [:vardef, "Stabilize1"], [:retval, [:trk, <T "track1" with 116 keyframes>, <T "track2" with 116 keyframes>, <T "track3" with 116 keyframes>]]]
    def atom_is_tracker_assignment?(a)
      deep_include?(a, :trk)
    end
    
    # For Linear() curve calls. If someone selected JSpline or Hermite it's his problem.
    # We put the frame number at the beginning since it works witih oru tuple zipper
    def linear(extrapolation_type, *keyframes)
      report_progress("Translating Linear animation")
      remap_keyframes_against_negative_at!(keyframes)
      keyframes.map { |kf| [kf.at , kf.value] }
    end
    alias_method :nspline, :linear
    alias_method :jspline, :linear
    
    # Hermite interpolation looks like this
    # Hermite(0,[1379.04,-0.02,-0.02]@1,[1379.04,-0.03,-0.03]@2)
    # The first value in the array is the keyframe value, the other two are
    # tangent positions (which we discard)
    def hermite(extrapolation_type, *keyframes)
      report_progress("Translating Hermite curve, removing tangents")
      remap_keyframes_against_negative_at!(keyframes)
      keyframes.map{ |kf| [kf.at, kf.value[0]] }
    end
    
    def remap_keyframes_against_negative_at!(kfs)
      frame_start_of_script = sentinel.start_frame
      kfs.each{|k| k.at = (k.at - frame_start_of_script) }
    end
    private :remap_keyframes_against_negative_at!
    
    # image Tracker( 
    #   image In,
    #   const char * trackRange,
    #   const char * subPixelRes,
    #   const char * matchSpace,
    #   float referenceTolerance,
    #   const char * referenceBehavior,
    #   float failureTolerance,
    #   const char * failureBehavior,
    #   int limitProcessing,
    #   float referencFrame 
    #   ...
    #  );  
    def tracker(input, trackRange, subPixelRes, matchSpace,
        referenceTolerance, referenceBehavior, failureTolerance, failureBehavior, limitProcessing, referencFrame, 
        s1, s2, s3, s4, s5, s6, *trackers)
      flat_tracks = if (s1 == "v2.0") # The Shake version stupid Winfucks users didn't get
        trackers
      else
        [s1, s2, s3, s4, s4, s6] + trackers
      end
      
      report_progress("Parsing Tracker node")
      collect_trackers_from(flat_tracks).unshift(:trk)
    end
    
    # stabilize {
    #   image In,
    #   int applyTransform,
    #   int inverseTransform
    #   const char * trackType,
    #   float track1X,
    #   float track1Y,
    #   int stabilizeX,
    #   int stabilizeY,
    #   float track2X,
    #   float track2Y,
    #   int matchScale,
    #   int matchRotation,
    #   float track3X,
    #   float track3Y,
    #   float track4X,
    #   float track4Y,
    #   const char * xFilter,
    #   const char * yFilter,
    #   const char * transformationOrder,
    #   float motionBlur, 
    #   float shutterTiming,
    #   float shutterOffset,
    #   float referenceFrame,
    #   float aspectRatio,
    #   ...
    # };
    def stabilize(imageIn, applyTransform, inverseTransform, trackType,
      track1X, track1Y,
      stabilizeX, stabilizeY,
      track2X, track2Y,
      matchScale,
      matchRotation,
      track3X, track3Y,
      track4X, track4Y,
      *useless_args)
      
      report_progress("Parsing Stabilize node")
      [
        collect_stabilizer_tracker("track1", track1X, track1Y),
        collect_stabilizer_tracker("track2", track2X, track2Y),
        collect_stabilizer_tracker("track3", track3X, track3Y),
        collect_stabilizer_tracker("track4", track4X, track4Y),
      ].compact.unshift(:trk)
    end
    
    #  image = MatchMove( 
    #    Background,
    #    Foreground,
    #    applyTransform,
    #    "trackType",
    #    track1X,
    #    track1Y,
    #    matchX,
    #    matchY,
    #    track2X,
    #    track2Y,
    #    scale,
    #    rotation,
    #    track3X,
    #    track3Y,
    #    track4X,
    #    track4Y,
    #    x1, 
    #    y1,
    #    x2,
    #    y2,
    #    x3,
    #    y3,
    #    x4,
    #    y4,
    #    "xFilter",
    #    "yFilter",
    #    motionBlur, 
    #    shutterTiming,
    #    shutterOffset,
    #    referenceFrame,
    #    "compositeType",
    #    clipMode,
    #   "trackRange",
    #    "subPixelRes",
    #    "matchSpace",
    #    float referenceTolerance,
    #    "referenceBehavior",
    #    float failureTolerance,
    #    "failureBehavior",
    #    int limitProcessing,
    #    ...
    #  );
    def matchmove(bgImage, fgImage, applyTransform,
      trackType,
      track1X,
      track1Y,
      matchX,
      matchY,
      track2X,
      track2Y,
      scale,
      rotation,
      track3X,
      track3Y,
      track4X,
      track4Y, *others)
      
      report_progress("Parsing MatchMove node")
      [
        collect_stabilizer_tracker("track1", track1X, track1Y),
        collect_stabilizer_tracker("track2", track2X, track2Y),
        collect_stabilizer_tracker("track3", track3X, track3Y),
        collect_stabilizer_tracker("track4", track4X, track4Y),
      ].compact.unshift(:trk)
    end
    
    private
    
    def report_progress(with_message)
      sentinel.progress_proc.call(with_message)
    end
    
    def collect_trackers_from(array)
      parameters_per_node = 16
      nb_trackers = array.length / parameters_per_node
      
      (0...nb_trackers).map do | idx |
        from_index, to_index = (idx * parameters_per_node), (idx+1) * parameters_per_node
        tracker_args = array[from_index...to_index]
        tracker_args[0] = "#{tracker_args[0]}"
        collect_tracker(*tracker_args)
      end.compact
    end
    
    # Remove tuples which have more than 2 values, and tuples that
    # have non-Numeric members
    def clean_tuples(frame_and_value_tuples)
      frame_and_value_tuples.reject do | element |
        element.length > 2
      end.reject do | element |
        element.any?{|e| !e.is_a?(Numeric) }
      end 
    end
    
    def collect_stabilizer_tracker(name, x_curve, y_curve)
      return unless valid_curves?(x_curve, y_curve)
      
      report_progress("Assembling Stabilizer node tracker #{name}")
      
      keyframes = zip_curve_tuples(clean_tuples(x_curve), clean_tuples(y_curve)).map do | (frame, x, y) |
        Tracksperanto::Keyframe.new(:frame => frame - 1, :abs_x => x, :abs_y => y)
      end
      Tracksperanto::Tracker.new(:name => name, :keyframes => keyframes)
    end
    
    def collect_tracker(name, x_curve, y_curve, corr_curve, *discard)
      return unless valid_curves?(x_curve, y_curve)
      
      report_progress("Scavenging tracker #{name}")
      
      curve_set = combine_curves(x_curve, y_curve, corr_curve)
      
      keyframes = zip_curve_tuples(*curve_set).map do | (frame, x, y, corr) |
        Tracksperanto::Keyframe.new(:frame => frame - 1, :abs_x => x, :abs_y => y, :residual => (1 - corr.to_f))
      end
      
      Tracksperanto::Tracker.new(:name => name, :keyframes => keyframes)
    end
    
    def combine_curves(x, y, corr_curve)
      curve_set = [x, y]
      curve_set << corr_curve if (corr_curve.respond_to?(:length) && corr_curve.length >= x.length)
      curve_set
    end
    
    private
    
    def valid_curves?(x_curve, y_curve)
      return false if (x_curve == :unknown || y_curve == :unknown)
      return false unless x_curve.is_a?(Array) && y_curve.is_a?(Array)
      true
    end
    
  end
end
