package li.cil.oc.common.recipe

import java.io.{File, FileReader}
import java.util.logging.Level

import com.typesafe.config._
import cpw.mods.fml.common.Loader
import cpw.mods.fml.common.registry.GameRegistry
import li.cil.oc.util.Color
import li.cil.oc.util.mods.GregTech
import li.cil.oc.{Items, OpenComputers, api, common}
import net.minecraft.block.Block
import net.minecraft.item.crafting.FurnaceRecipes
import net.minecraft.item.{Item, ItemStack}
import net.minecraftforge.oredict.OreDictionary
import org.apache.commons.io.FileUtils

import scala.collection.convert.WrapAsScala._
import scala.collection.mutable

object Recipes {
  val list = mutable.LinkedHashMap.empty[ItemStack, String]
  val oreDictEntries = mutable.LinkedHashMap.empty[String, ItemStack]

  def addBlock[T <: common.block.Delegate](delegate: T, name: String, oreDict: String = null) = {
    Items.registerBlock(delegate, name)
    list += delegate.createItemStack() -> name
    register(oreDict, delegate.createItemStack())
    delegate
  }

  def addItem[T <: common.item.Delegate](delegate: T, name: String, oreDict: String = null) = {
    Items.registerItem(delegate, name)
    list += delegate.createItemStack() -> name
    register(oreDict, delegate.createItemStack())
    delegate
  }

  def addItem(instance: Item, name: String) = {
    Items.registerItem(instance, name)
    list += new ItemStack(instance) -> name
    instance
  }

  private def register(name: String, item: ItemStack) {
    if (name != null) {
      oreDictEntries += name -> item
    }
  }

  def init() {
    for ((name, stack) <- oreDictEntries) {
      if (!OreDictionary.getOres(name).contains(stack)) {
        OreDictionary.registerOre(name, stack)
      }
    }
    oreDictEntries.clear()

    try {
      val defaultRecipes = new File(Loader.instance.getConfigDir + File.separator + "opencomputers" + File.separator + "default.recipes")
      val hardmodeRecipes = new File(Loader.instance.getConfigDir + File.separator + "opencomputers" + File.separator + "hardmode.recipes")
      val gregTechRecipes = new File(Loader.instance.getConfigDir + File.separator + "opencomputers" + File.separator + "gregtech.recipes")
      val userRecipes = new File(Loader.instance.getConfigDir + File.separator + "opencomputers" + File.separator + "user.recipes")

      defaultRecipes.getParentFile.mkdirs()
      FileUtils.copyURLToFile(getClass.getResource("/assets/opencomputers/recipes/default.recipes"), defaultRecipes)
      FileUtils.copyURLToFile(getClass.getResource("/assets/opencomputers/recipes/hardmode.recipes"), hardmodeRecipes)
      FileUtils.copyURLToFile(getClass.getResource("/assets/opencomputers/recipes/gregtech.recipes"), gregTechRecipes)
      if (!userRecipes.exists()) {
        FileUtils.copyURLToFile(getClass.getResource("/assets/opencomputers/recipes/user.recipes"), userRecipes)
      }
      lazy val config: ConfigParseOptions = ConfigParseOptions.defaults.
        setSyntax(ConfigSyntax.CONF).
        setIncluder(new ConfigIncluder with ConfigIncluderFile {
        var fallback: ConfigIncluder = _

        override def withFallback(fallback: ConfigIncluder) = {
          this.fallback = fallback
          this
        }

        override def include(context: ConfigIncludeContext, what: String) = fallback.include(context, what)

        override def includeFile(context: ConfigIncludeContext, what: File) = {
          val in = if (what.isAbsolute) new FileReader(what) else new FileReader(new File(userRecipes.getParentFile, what.getPath))
          val result = ConfigFactory.parseReader(in, config)
          in.close()
          result.root()
        }
      })
      val recipes = ConfigFactory.parseFile(userRecipes, config)

      // Register all known recipes.
      for ((stack, name) <- list) {
        addRecipe(stack, recipes, name)
      }

      // Navigation upgrade recrafting.
      val navigationUpgrade = api.Items.get("navigationUpgrade").createItemStack(1)
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(navigationUpgrade, navigationUpgrade, new ItemStack(Item.map, 1, OreDictionary.WILDCARD_VALUE)))

      // Floppy disk coloring.
      val floppy = api.Items.get("floppy").createItemStack(1)
      for (dye <- Color.dyes) {
        GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(floppy, floppy, dye))
      }
    }
    catch {
      case e: Throwable => OpenComputers.log.log(Level.SEVERE, "Error parsing recipes, you may not be able to craft any items from this mod!", e)
    }
    list.clear()
  }

  private def addRecipe(output: ItemStack, list: Config, name: String) = try {
    if (list.hasPath(name)) {
      val recipe = list.getConfig(name)
      val recipeType = tryGetType(recipe)
      try {
        recipeType match {
          case "shaped" => addShapedRecipe(output, recipe)
          case "shapeless" => addShapelessRecipe(output, recipe)
          case "furnace" => addFurnaceRecipe(output, recipe)
          case "assembly" => addAssemblyRecipe(output, recipe)
          case other =>
            OpenComputers.log.warning("Failed adding recipe for '" + name + "', you will not be able to craft this item! The error was: Invalid recipe type '" + other + "'.")
            hide(output)
        }
      }
      catch {
        case e: RecipeException =>
          OpenComputers.log.warning("Failed adding " + recipeType + " recipe for '" + name + "', you will not be able to craft this item! The error was: " + e.getMessage)
          hide(output)
      }
    }
    else {
      OpenComputers.log.info("No recipe for '" + name + "', you will not be able to craft this item.")
      hide(output)
    }
  }
  catch {
    case e: Throwable =>
      OpenComputers.log.log(Level.SEVERE, "Failed adding recipe for '" + name + "', you will not be able to craft this item!", e)
      hide(output)
  }

  private def addShapedRecipe(output: ItemStack, recipe: Config) {
    val rows = recipe.getList("input").unwrapped().map {
      case row: java.util.List[AnyRef]@unchecked => row.map(parseIngredient)
      case other => throw new RecipeException("Invalid row entry for shaped recipe (not a list: " + other + ").")
    }
    output.stackSize = tryGetCount(recipe)

    var number = -1
    var shape = mutable.ArrayBuffer.empty[String]
    val input = mutable.ArrayBuffer.empty[AnyRef]
    for (row <- rows) {
      val (pattern, ingredients) = row.foldLeft((new StringBuilder, Seq.empty[AnyRef]))((acc, ingredient) => {
        val (pattern, ingredients) = acc
        ingredient match {
          case _@(_: ItemStack | _: String) =>
            number += 1
            (pattern.append(('a' + number).toChar), ingredients ++ Seq(Char.box(('a' + number).toChar), ingredient))
          case _ => (pattern.append(' '), ingredients)
        }
      })
      shape += pattern.toString
      input ++= ingredients
    }
    if (input.size > 0 && output.stackSize > 0) {
      GameRegistry.addRecipe(new ExtendedShapedOreRecipe(output, shape ++ input: _*))
    }
    else hide(output)
  }

  private def addShapelessRecipe(output: ItemStack, recipe: Config) {
    val input = recipe.getValue("input").unwrapped() match {
      case list: java.util.List[AnyRef]@unchecked => list.map(parseIngredient)
      case other => Seq(parseIngredient(other))
    }
    output.stackSize = tryGetCount(recipe)

    if (input.size > 0 && output.stackSize > 0) {
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(output, input: _*))
    }
    else hide(output)
  }

  private def addAssemblyRecipe(output: ItemStack, recipe: Config) {
    val input = (recipe.getValue("input").unwrapped() match {
      case list: java.util.List[AnyRef]@unchecked => list.map(parseIngredient)
      case other => Seq(parseIngredient(other))
    }) map {
      case stack: ItemStack => stack
      case null => null
      case name: String => throw new RecipeException("Invalid ingredient '" + name + "', OreDictionary not supported for assembly recipes.")
      case other => throw new RecipeException("Invalid ingredient type: " + other + ".")
    }
    output.stackSize = tryGetCount(recipe)

    if (input.size < 1 || input.size > 2) {
      throw new RecipeException("Invalid recipe length: " + input.size + ", should be 1 or 2.")
    }

    val inputCount = recipe.getIntList("count")
    if (inputCount.size() != input.size) {
      throw new RecipeException("Ingredient and input count mismatch: " + input.size + " != " + inputCount.size + ".")
    }

    val eu = recipe.getInt("eu")
    val duration = recipe.getInt("time")

    (input, inputCount).zipped.foreach((stack, count) => if (stack != null && count > 0) stack.stackSize = stack.getMaxStackSize min count)
    input.padTo(2, null)

    if (input(0) != null) {
      GregTech.addAssemblerRecipe(input(0), input(1), output, duration, eu)
    }
  }

  private def addFurnaceRecipe(output: ItemStack, recipe: Config) {
    val input = parseIngredient(recipe.getValue("input").unwrapped())
    output.stackSize = tryGetCount(recipe)

    input match {
      case stack: ItemStack =>
        FurnaceRecipes.smelting().addSmelting(stack.itemID, stack.getItemDamage, output, 0)
      case name: String =>
        for (stack <- OreDictionary.getOres(name)) {
          FurnaceRecipes.smelting().addSmelting(stack.itemID, stack.getItemDamage, output, 0)
        }
      case _ =>
    }
  }

  private def parseIngredient(entry: AnyRef) = entry match {
    case map: java.util.Map[AnyRef, AnyRef]@unchecked =>
      if (map.contains("oreDict")) {
        map.get("oreDict") match {
          case value: String => value
          case other => throw new RecipeException("Invalid name in recipe (not a string: " + other + ").")
        }
      }
      else if (map.contains("item")) {
        map.get("item") match {
          case name: String =>
            Item.itemsList.find(itemNameEquals(_, name)) match {
              case Some(item) => new ItemStack(item, 1, tryGetId(map))
              case _ => throw new RecipeException("No item found with name '" + name + "'.")
            }
          case id: Number => new ItemStack(validateItemId(id), 1, tryGetId(map))
          case other => throw new RecipeException("Invalid item name in recipe (not a string: " + other + ").")
        }
      }
      else if (map.contains("block")) {
        map.get("block") match {
          case name: String =>
            Block.blocksList.find(blockNameEquals(_, name)) match {
              case Some(block) => new ItemStack(block, 1, tryGetId(map))
              case _ => throw new RecipeException("No block found with name '" + name + "'.")
            }
          case id: Number => new ItemStack(validateBlockId(id), 1, tryGetId(map))
          case other => throw new RecipeException("Invalid block name (not a string: " + other + ").")
        }
      }
      else throw new RecipeException("Invalid ingredient type (no oreDict, item or block entry).")
    case name: String =>
      if (name == null || name.trim.isEmpty) null
      else if (OreDictionary.getOres(name) != null && !OreDictionary.getOres(name).isEmpty) name
      else {
        Item.itemsList.find(itemNameEquals(_, name)) match {
          case Some(item) => new ItemStack(item, 1, 0)
          case _ => Block.blocksList.find(blockNameEquals(_, name)) match {
            case Some(block) => new ItemStack(block, 1, 0)
            case _ => throw new RecipeException("No ore dictionary entry, item or block found for ingredient with name '" + name + "'.")
          }
        }
      }
    case other => throw new RecipeException("Invalid ingredient type (not a map or string): " + other)
  }

  private def itemNameEquals(item: Item, name: String) =
    item != null && (item.getUnlocalizedName == name || item.getUnlocalizedName == "item." + name)

  private def blockNameEquals(block: Block, name: String) =
    block != null && (block.getUnlocalizedName == name || block.getUnlocalizedName == "tile." + name)

  private def tryGetType(recipe: Config) = if (recipe.hasPath("type")) recipe.getString("type") else "shaped"

  private def tryGetCount(recipe: Config) = if (recipe.hasPath("output")) recipe.getInt("output") else 1

  private def tryGetId(ingredient: java.util.Map[AnyRef, AnyRef]): Int =
    if (ingredient.contains("subID")) ingredient.get("subID") match {
      case id: Number => id.intValue
      case "any" => OreDictionary.WILDCARD_VALUE
      case id: String => Integer.valueOf(id)
      case _ => 0
    } else 0

  private def validateBlockId(id: Number) = {
    val index = id.intValue
    if (index < 1 || index >= Block.blocksList.length || Block.blocksList(index) == null) throw new RecipeException("Invalid block ID: " + index)
    Block.blocksList(index)
  }

  private def validateItemId(id: Number) = {
    val index = id.intValue
    if (index < 0 || index >= Item.itemsList.length || Item.itemsList(index) == null) throw new RecipeException("Invalid item ID: " + index)
    Item.itemsList(index)
  }

  private def hide(value: ItemStack) {
    Items.multi.subItem(value) match {
      case Some(stack) => stack.showInItemList = false
      case _ => common.block.Delegator.subBlock(value) match {
        case Some(block) => block.showInItemList = false
        case _ =>
      }
    }
  }

  private class RecipeException(message: String) extends RuntimeException(message)

}
