#################################
### COPYRIGHT: Anders Lyhagen ###
#################################

#set up all datastructures in a FlowData object
module CAUL_FAQS
module Input

  @TARGET_SCALE = 24000
  @ERROR_STRING = ''
  @parse_err = "\n"+'The grid must conform to:'+"\n\t"+
                '- A cell consists of exactly four edges. Beware of collinear edge segments'+"\n\t"+
                '- Connection edges must attatch to two adjecent corners'+"\n\t"+
                '- The grid must have four corners'+"\n\t"+
                '- Every row in the grid must have the same number of cells'+"\n\t"+
                '- No holes'
               

  #Parse the support group ('we may need a parameter here telling whether we look for
  #inverse intersection. 
  def self.parseSupportGroup(sg, fd)    
    pg = nil #ProjectionPlaneGroup
    cg = nil #Connection Group
    tg = nil #Target group
    fd.sg = sg
    
    gs = sg.entities.grep(Sketchup::Group)
    #This should not happen
    return false if gs.length != 3
    
    cg = getConnectionGroup(gs) #cg is deleted from gs..
    #This should not happen either...
    return false if cg == nil
    
    #among the remaining groups, sort out the projgrid and the target surface
    ss = determineSurfaces(gs, fd, sg)
    return false if ss[0] == nil
    pg = ss[0]
    tg = ss[1]
     
    #untangle the constructionLine group and produce two sets of
    cls = cg.entities.grep(Sketchup::ConstructionLine) + cg.entities.grep(Sketchup::Edge)
    pgp = []; tgp = []
    pgp << getVertex(pg, cls[0], cg.transformation)
    pgp << getVertex(pg, cls[1], cg.transformation)
    tgp << getVertex(tg, cls[0], cg.transformation)
    tgp << getVertex(tg, cls[1], cg.transformation)
    
    #ERROR: 
    if pgp[0] == nil || pgp[1] == nil || tgp[0] == nil || tgp[1] == nil
      @ERROR_STRING = 'INPUT ERROR: Connection edge does not connect properly between vertices'
      return false
    end
    
    a = corr_oriented?(pg, pgp[0][0], pgp[1][0]) ? 0 : 1
    begin
      fd.tgr = GridParser.parse(tg.entities, tgp[a][1], tgp[a - 1][1], nil)
    rescue
      @ERROR_STRING = 'INPUT ERROR: Error while parsing the Target grid'+@parse_err
      return false
    end
    
    begin
      fd.pgr = GridParser.parse(pg.entities, pgp[a][1], pgp[a - 1][1], fd.tgr)
    rescue
      @ERROR_STRING = 'INPUT ERROR: Error while parsing the projection grid'
      return false
    end
    
    begin
      fd.pgr = reconstructProjGrid(fd.tgr, fd.pgr, pg, pgp[a][1], pgp[a - 1][1])
    rescue
      @ERROR_STRING = 'INPUT ERROR: Error while reconstructing the projection grid'
      return false
    end
    
    unless correlateGrids(fd.pgr, fd.tgr)
      @ERROR_STRING = 'INPUT ERROR: The target grid and projection grid have different topologies'
      return nil
    end
    
    unless validateKinds(fd.tgr, fd.pgr)
      @ERROR_STRING = 'INPUTE ERROR: The kinds do not match: Bug in script. Contact author'
      return false
    end
    
    fd.pg = pg
    fd.tg = tg
    fd.pgr_out = sg.transformation * pg.transformation
    fd.pgr_in =  pg.transformation.inverse * sg.transformation.inverse
    fd.tgr_out = sg.transformation * tg.transformation
    fd.tgr_in =  tg.transformation.inverse * sg.transformation.inverse
    
    fs = fd.pg.entities.grep(Sketchup::Face)
    fd.proj_ve = fs[0].normal.transform(fd.pgr_out).reverse
    fd.proj_pt = fs[0].vertices[0].position.transform(fd.pgr_out)
    
    max = sg.bounds.diagonal
    fd.scale = max < @TARGET_SCALE ? @TARGET_SCALE / max : 1
    return true
  end
  
  #gs contains two groups with two grids, determine which one serves which function (proj or target)
  def self.determineSurfaces(gs, fd, sg)
    flat0 = flat?(gs[0])
    flat1 = flat?(gs[1])
    if flat0 ^ flat1
      return (flat0 ? [gs[0], gs[1]] : [gs[1], gs[0]])
    elsif flat0 && flat1
      fs0 = gs[0].entities.grep(Sketchup::Face)
      fs1 = gs[1].entities.grep(Sketchup::Face)
      return [gs[0], gs[1]] if fs0.length == 1 && fs1.length > 1
      return [gs[1], gs[0]] if fs1.length == 1  && fs0.length > 1
      return [nil, nil] if fd == nil || fd.ggs == nil
      s0c = gs[0].bounds.center.transform(sg.transformation)
      s1c = gs[1].bounds.center.transform(sg.transformation)
      ggc = fd.ggs[0].bounds.center
      return [gs[0], gs[1]] if s0c.distance(ggc) < s1c.distance(ggc) 
      return [gs[1], gs[0]]
    else
      @ERROR_STRING = 'INPUTE ERROR: Projection Grid must be flat'
      return [nil, nil]
    end
  end
  
  def self.flat?(g)
    es = g.entities.grep(Sketchup::Edge)
    vs = es.map { |e| e.vertices }.flatten!.uniq!
    ps = vs.map { |v| v.position }
    pl = Geom::fit_plane_to_points(ps)
    return ps.all? { |p| p.on_plane? pl }
  end
  
  def self.validateKinds(tgr, pgr)
    (0..pgr.grid.length - 1).each { |u|
      (0..pgr.grid[0].length - 1).each { |v|
        return false if tgr.grid[u][v].kind != pgr.grid[u][v].kind
    }}
    return true
  end
  
  #Makes sure that the grid is flat
  def self.validateProjGrid(pg)
    es = pg.entities.grep(Sketchup::Edge)
    vs = es.map { |e| e.vertices }.flatten!.uniq!
    ps = vs.map { |v| v.position }
    
    #if es.any?{ |e| e.soft? }
    #  @ERROR_STRING = 'INPUTE ERROR: Projection Grid can have no diagonals or softened edges'
    #  return false
    #end
    
    pl = Geom::fit_plane_to_points(ps)
    unless ps.all? { |p| p.on_plane? pl }
      @ERROR_STRING = 'INPUTE ERROR: Projection Grid must be flat'
      return false
    end
  
    return true
  end
  
  def self.reconstructProjGrid(tgr, pgr, pg, pg0, pg1)
    #if the grids have identical topology, then do nothing
    return pgr if pgr.grid.length == tgr.grid.length && pgr.grid[0].length == tgr.grid[0].length
    return pgr if pgr.grid.length != 1 || pgr.grid[0].length != 1 #error, will be cathed later
    
    gu = tgr.grid.length
    gv = tgr.grid[0].length
    p0 = pgr.getPointOnGrid(0, 0); p1 = pgr.getPointOnGrid(1, 0)
    p2 = pgr.getPointOnGrid(1, 1); p3 = pgr.getPointOnGrid(0, 1)
    du0 = p1 - p0; du0.length = p0.distance(p1) / gu 
    du1 = p2 - p3; du1.length = p2.distance(p3) / gu
    uls = (0..gu).inject([]) { |a, u| a << [offset(p0, du0, u), offset(p3, du1, u)]}
    
    pm = Geom::PolygonMesh.new()
    (0..gu-1).each { |u|
      d0 = (uls[u][0].distance(uls[u][1]) / gv)
      n0 = (uls[u][1] - uls[u][0]).normalize
      d1 = (uls[u + 1][0].distance(uls[u + 1][1]) / gv)
      n1 = (uls[u + 1][1] - uls[u + 1][0]).normalize
      
      (0..gv-1).each { |v|
        ps = []
        ps << offset(uls[u][0], n0, d0 * v)
        ps << offset(uls[u][0], n0, d0 * (v + 1))
        ps << offset(uls[u + 1][0], n1, d1 * (v + 1))
        ps << offset(uls[u + 1][0], n1, d1 * v)
        ps.each { |p| pm.add_point(p) } 
        pm.add_polygon(ps)
    }}
    pg.entities.clear!
    pg.entities.fill_from_mesh(pm, true, 0, nil)
    
    return GridParser.parse(pg.entities, pg0, pg1, tgr)
  end
  
  def self.offset(p, v, d)
    pc = p.clone
    pc.x = pc.x + v.x * d
    pc.y = pc.y + v.y * d
    pc.z = pc.z + v.z * d
    return pc
  end

  def self.corr_oriented?(g, v0, v1)
    c = g.bounds.center.transform(g.transformation.inverse)
    fs = g.entities.grep(Sketchup::Face)
    return (v1.position - c).cross(v0.position - c).dot(fs[0].normal) > 0 
  end
  
  #Find the vertex in g corresponding to a vertex in the
  #connection line.
  def self.getVertex(g, cl, cltr)
    p0 = cl.is_a?(Sketchup::Edge) ? cl.start.position : cl.start
    p1 = cl.is_a?(Sketchup::Edge) ? cl.end.position : cl.end
    tr = g.transformation.inverse * cltr
    es = g.entities.grep(Sketchup::Edge)
    vs = es.map { |e| e.vertices }.flatten!.uniq!
    vs.each { |v|
      return [v, p0.transform(tr)] if v.position == p0.transform(tr)
      return [v, p1.transform(tr)] if v.position == p1.transform(tr)
    }
    return nil
  end
  
  def self.correlateGrids(p_grid, t_grid)
    unless p_grid.u_map.length == t_grid.u_map.length && p_grid.v_map.length == t_grid.v_map.length
      return false
    end
    return true
  end  

  #Connection group may contain at most two entities..
  #Either edges, constructionlines or a combination..
  def self.getConnectionGroup(gs)
    #find the connection group
    (0..gs.length - 1).each { |i|
      if gs[i].entities.length == 2
        cls = gs[i].entities.grep(Sketchup::ConstructionLine)
        es = gs[i].entities.grep(Sketchup::Edge)
        return gs.delete_at(i) if (cls.length + es.length) == 2
      end
    }
    return nil
  end
  
  def self.getErrorString
    return @ERROR_STRING
  end
  
  def self.formatString(s, len) 
    sp = ''
    (0..(len - s.length - 1)).each { |i| sp += ' ' }
    return s + sp
  end
  
  def self.parseFixPoints(fd)
    fd.fixs = []
    fd.ggs.each { |g|
      cs = g.entities.grep(Sketchup::ConstructionPoint)
      if cs.length == 0
        fd.fixs << getDefaultFixPoint(g)
      else
        fd.fixs << cs[0].position.transform(g.transformation)
      end
    }
  end

  #change this method...not valid since we can't assume alignment with y axis.
  #chose bounding box center and project it onto the projplane alnog the plane normal.
  def self.getDefaultFixPoint(g)
    bb = g.bounds
    x = (bb.max.x-bb.min.x) / 2.0 + bb.min.x
    y = (bb.max.y-bb.min.y) / 2.0 + bb.min.y
    z = (bb.max.z-bb.min.z) / 2.0 + bb.min.z
    return Geom::Point3d.new(x, y, z)
  end
  
  def self.getGroup(c, ent)
    ng = ent.add_group
    nc = ng.entities.add_instance(c.definition, c.transformation)
    nc.explode if nc != nil
    return ng
  end

  
  def self.getSupportGroup(gs)
    ggs = []
    sg = nil
    #extract all groups that contain further groups
    gs.each { |g| ggs << g if g.entities.grep(Sketchup::Group).length > 0}
    ggs.each { |g|
      sggs = g.entities.grep(Sketchup::Group)
      next if sggs.length != 3
      sggs.each { |g2| 
        if g2.entities.length == 2
          ces = g2.entities.grep(Sketchup::ConstructionLine)
          es = g2.entities.grep(Sketchup::Edge)
          next if (ces.length + es.length) != 2
          sg = g
          break;
        end
      }
      break if sg != nil
    }
    if sg == nil
      @ERROR_STRING = 'INPUT ERROR: Could not find support group. The support Group must contain three further groups and one of these groups must contain two connection edges'
      return nil
    end
        
    gs.delete(sg)
    return sg
  end
  
  def self.validateGroup(g)
    return g.entities.any? { |ent| ent.is_a?(Sketchup::Face) }
  end
  
  def self.validateComponent(c)
    return c.definition.entities.any? { |ent| ent.is_a?(Sketchup::Face) }
  end
  

  #Public interface Method
  def self.parseInput(sel, ent)
  
    gs = sel.grep(Sketchup::Group)
    cs = sel.grep(Sketchup::ComponentInstance)
    
    #ERROR 1
    if gs.length < 1 || gs.length + cs.length < 2
      @ERROR_STRING = 'INPUT ERROR: Not enough Input'
      return nil
    end
    
    gs2 = gs.clone
    sg = getSupportGroup(gs2) #the support group is deleted from gs2 here..
    
    #ERROR (measseg already output in getSupportGroup)
    if sg == nil
      return nil
    end
    
    fd = FlowData.new
    fd.ggs = []
    fd.bb = Geom::BoundingBox.new
    
    if gs2.length > 0 && gs2.any? { |g| !validateGroup(g) }
      @ERROR_STRING = 'Source geometry group must contain raw faces'
      return nil
    end
    
    if cs.length > 0 && cs.any? { |c| !validateComponent(c) }
      @ERROR_STRING = 'Source Geometry component must contain raw faces'
      return nil
    end
    
    (gs2 + cs).each { |g|
      fd.bb.add(g.bounds)
      fd.ggs << g
    }
    
    
    unless parseSupportGroup(sg, fd)
      return nil
    end
    
  
    fd.ggs = []
    (gs2 + cs) .each { |g| 
      ng = (g.is_a?(Sketchup::Group) ? g.copy : getGroup(g, ent))
      fd.ggs << ng
    }
    parseFixPoints(fd)
    return fd
  end
  
  def self.imposeGrid(sel)
    gs = sel.grep(Sketchup::Group)
    return true if gs.length != 1
    sg = getSupportGroup(gs)
    return true if sg == nil
    parseSupportGroup(sg, FlowData.new)
    return true
  end

  def self.main
    mod = Sketchup.active_model # Open model
    ent = mod.entities # All entities in model
    sel = mod.selection # Current selection
    
    t0 = Time.now
    fd = parseInput(sel, ent)
    
    t1 = Time.now
    tot_t = (t1 - t0)
    
    if fd == nil
      puts 'Fail '+tot_t.to_s+' s'
      puts @ERROR_STRING.to_s
    elsif
      fd.ggs.each { |g| g.erase! }
      puts 'Success '+tot_t.to_s+' s'
    end
  end
 
end
end



