///|
/// D4 symmetry names and orientations follow mxgmn/WaveFunctionCollapse (MIT).
/// Neighbor expansion below applies group actions to a directed spatial edge.
pub(all) struct TileDefinition {
  name : String
  symmetry : String
  weight : Double
  pixels : Array[Array[Int]]
} derive(Debug, ToJson)

///|
pub struct TiledModel {
  rules : RuleModel
  tiles : Array[Array[Int]]
  tile_size : Int
} derive(Debug, ToJson)

///|
fn symmetry_count(symmetry : String) -> Int raise SolveError {
  match symmetry {
    "X" => 1
    "I" | "\\" => 2
    "L" | "T" => 4
    "F" => 8
    _ => raise Invalid("unknown tile symmetry")
  }
}

///|
fn rotate_orientation(symmetry : String, orientation : Int) -> Int {
  match symmetry {
    "X" => 0
    "I" | "\\" => 1 - orientation
    "F" =>
      if orientation < 4 {
        (orientation + 1) % 4
      } else {
        4 + (orientation + 3) % 4
      }
    _ => (orientation + 1) % 4
  }
}

///|
fn reflect_orientation(symmetry : String, orientation : Int) -> Int {
  match symmetry {
    "X" | "I" => orientation
    "\\" => 1 - orientation
    "L" => if orientation % 2 == 0 { orientation + 1 } else { orientation - 1 }
    "T" => if orientation % 2 == 0 { orientation } else { 4 - orientation }
    _ => if orientation < 4 { orientation + 4 } else { orientation - 4 }
  }
}

///|
pub fn expand_tiles(
  definitions : Array[TileDefinition],
  tile_size : Int,
  neighbors : Array[(String, String)],
  subset? : Array[String] = [],
) -> TiledModel raise SolveError {
  if tile_size < 1 ||
    tile_size > 256 ||
    definitions.is_empty() ||
    definitions.length() > 4096 ||
    neighbors.length() > 100000 {
    raise Invalid("invalid tileset dimensions")
  }
  let names : Map[String, Int] = Map([])
  let starts : Map[String, Int] = Map([])
  let actions : Array[Array[Int]] = []
  let labels = []
  let weights = []
  let tiles : Array[Array[Int]] = []
  for definition in definitions {
    if definition.name.is_empty() ||
      definition.name
      .iter()
      .any(c => c == ' ' || c == '\n' || c == '\t' || c == '\r') ||
      names.contains(definition.name) {
      raise Invalid("empty, duplicate or whitespace tile name")
    }
    names[definition.name] = 1
    let cardinality = symmetry_count(definition.symmetry)
    if !subset.is_empty() && !subset.contains(definition.name) {
      continue
    }
    if definition.pixels.length() != 1 &&
      definition.pixels.length() != cardinality {
      raise Invalid("tile needs one base bitmap or all orientation bitmaps")
    }
    if definition.pixels.iter().any(p => p.length() != tile_size * tile_size) {
      raise Invalid("tile bitmap dimensions differ")
    }
    let start = labels.length()
    if start + cardinality > 4096 ||
      (start + cardinality).to_int64() *
      tile_size.to_int64() *
      tile_size.to_int64() >
      4000000L {
      raise Invalid("expanded tile data limit")
    }
    starts[definition.name] = start
    for orientation in 0.. {
          tiles[start + orientation - 1][i % tile_size * tile_size +
          tile_size -
          1 -
          i / tile_size]
        })
      } else {
        Array::makei(tile_size * tile_size, i => {
          tiles[start + orientation - 4][i / tile_size * tile_size +
          tile_size -
          1 -
          i % tile_size]
        })
      }
      tiles.push(pixel)
      labels.push(definition.name + " " + orientation.to_string())
      weights.push(definition.weight)
    }
  }
  for name in subset {
    if !names.contains(name) {
      raise Invalid("unknown subset tile")
    }
  }
  let edges : Array[Map[Int, Bool]] = Array::makei(labels.length() * 4, _ => {
    Map([])
  })
  fn reference(text : String) -> (String, Int) raise SolveError {
    let parts = text.split(" ").filter(p => !p.is_empty()).collect()
    if parts.is_empty() || parts.length() > 2 {
      raise Invalid("invalid neighbor reference")
    }
    let name = parts[0].to_owned()
    if !names.contains(name) {
      raise Invalid("unknown neighbor tile")
    }
    let orientation = if parts.length() == 1 {
      0
    } else if parts[1].length() == 1 && parts[1][0] >= '0' && parts[1][0] <= '7' {
      parts[1][0].to_int() - 48
    } else {
      raise Invalid("neighbor orientation must be 0..7")
    }
    (name, orientation)
  }
  for (left, right) in neighbors {
    let l = reference(left)
    let r = reference(right)
    if starts.get(l.0) is Some(ls) && starts.get(r.0) is Some(rs) {
      let left_id = actions[ls][l.1]
      let right_id = actions[rs][r.1]
      for mirror in 0..<2 {
        for turn in 0..<4 {
          let a = actions[left_id][turn + 4 * mirror]
          let b = actions[right_id][turn + 4 * mirror]
          let rotated = (4 - turn) % 4
          let direction = if mirror == 1 && rotated % 2 == 0 {
            (rotated + 2) % 4
          } else {
            rotated
          }
          edges[a * 4 + direction][b] = true
          edges[b * 4 + (direction + 2) % 4][a] = true
        }
      }
    }
  }
  let neighbors = Array::makei(labels.length(), tile => {
    Array::makei(4, direction => {
      let row = edges[tile * 4 + direction].keys().collect()
      row.sort()
      row
    })
  })
  let rules : RuleModel = { labels, weights, neighbors, }
  rules.checked()
  { rules, tiles, tile_size, }
}

///|
pub fn TiledModel::render(
  self : TiledModel,
  solution : Solution,
) -> Array[Int] raise SolveError {
  if self.tile_size < 1 ||
    self.tile_size > 256 ||
    self.tiles.length() != self.rules.labels.length() ||
    self.tiles.iter().any(t => t.length() != self.tile_size * self.tile_size) ||
    !solution.validate_rules(self.rules) ||
    solution.tiles.length().to_int64() *
    self.tile_size.to_int64() *
    self.tile_size.to_int64() >
    4000000L {
    raise Invalid("invalid tiled rendering dimensions or solution")
  }
  let width = solution.width * self.tile_size
  Array::makei(solution.tiles.length() * self.tile_size * self.tile_size, i => {
    let x = i % width
    let y = i / width
    self.tiles[solution.tiles[y / self.tile_size * solution.width +
    x / self.tile_size]][y % self.tile_size * self.tile_size +
    x % self.tile_size]
  })
}