package li.cil.oc.common.recipe

import java.io.File
import java.io.FileReader

import com.typesafe.config._
import cpw.mods.fml.common.Loader
import cpw.mods.fml.common.registry.GameRegistry
import li.cil.oc._
import li.cil.oc.common.Loot
import li.cil.oc.common.block.SimpleBlock
import li.cil.oc.common.init.Items
import li.cil.oc.common.item.Delegator
import li.cil.oc.common.item.data.PrintData
import li.cil.oc.common.item.traits.Delegate
import li.cil.oc.common.item.traits.SimpleItem
import li.cil.oc.integration.util.NEI
import li.cil.oc.util.Color
import net.minecraft.block.Block
import net.minecraft.item.Item
import net.minecraft.item.ItemBlock
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.util.RegistryNamespaced
import net.minecraftforge.fluids.FluidRegistry
import net.minecraftforge.fluids.FluidStack
import net.minecraftforge.oredict.OreDictionary
import net.minecraftforge.oredict.RecipeSorter
import net.minecraftforge.oredict.RecipeSorter.Category
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]
  var hadErrors = false
  val recipeHandlers = mutable.LinkedHashMap.empty[String, (ItemStack, Config) => Unit]

  def registerRecipeHandler(name: String, recipe: (ItemStack, Config) => Unit): Unit = {
    recipeHandlers += name -> recipe
  }

  def addBlock(instance: Block, name: String, oreDict: String*) = {
    Items.registerBlock(instance, name)
    addRecipe(new ItemStack(instance), name)
    register(instance match {
      case simple: SimpleBlock => simple.createItemStack()
      case _ => new ItemStack(instance)
    }, oreDict: _*)
    instance
  }

  def addSubItem[T <: Delegate](delegate: T, name: String, oreDict: String*) = {
    Items.registerItem(delegate, name)
    addRecipe(delegate.createItemStack(), name)
    register(delegate.createItemStack(), oreDict: _*)
    delegate
  }

  def addItem(instance: Item, name: String, oreDict: String*) = {
    Items.registerItem(instance, name)
    addRecipe(new ItemStack(instance), name)
    register(instance match {
      case simple: SimpleItem => simple.createItemStack()
      case _ => new ItemStack(instance)
    }, oreDict: _*)
    instance
  }

  def addStack(stack: ItemStack, name: String, oreDict: String*) = {
    Items.registerStack(stack, name)
    addRecipe(stack, name)
    register(stack, oreDict: _*)
    stack
  }

  def addRecipe(stack: ItemStack, name: String) {
    list += stack -> name
  }

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

  def init() {
    RecipeSorter.register(Settings.namespace + "extshaped", classOf[ExtendedShapedOreRecipe], Category.SHAPED, "after:forge:shapedore")
    RecipeSorter.register(Settings.namespace + "extshapeless", classOf[ExtendedShapelessOreRecipe], Category.SHAPELESS, "after:forge:shapelessore")
    RecipeSorter.register(Settings.namespace + "colorizer", classOf[ColorizeRecipe], Category.SHAPELESS, "after:forge:shapelessore")
    RecipeSorter.register(Settings.namespace + "decolorizer", classOf[DecolorizeRecipe], Category.SHAPELESS, "after:oc:colorizer")
    RecipeSorter.register(Settings.namespace + "lootcycler", classOf[LootDiskCyclingRecipe], Category.SHAPELESS, "after:forge:shapelessore")

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

    try {
      val recipeSets = Array("default", "hardmode", "gregtech", "peaceful")
      val recipeDirectory = new File(Loader.instance.getConfigDir + File.separator + "opencomputers")
      val userRecipes = new File(recipeDirectory, "user.recipes")
      userRecipes.getParentFile.mkdirs()
      if (!userRecipes.exists()) {
        FileUtils.copyURLToFile(getClass.getResource("/assets/opencomputers/recipes/user.recipes"), userRecipes)
      }
      for (recipeSet <- recipeSets) {
        FileUtils.copyURLToFile(getClass.getResource(s"/assets/opencomputers/recipes/$recipeSet.recipes"), new File(recipeDirectory, s"$recipeSet.recipes"))
      }
      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) {
        if (recipes.hasPath(name)) {
          val value = recipes.getValue(name)
          value.valueType match {
            case ConfigValueType.OBJECT =>
              addRecipe(stack, recipes.getConfig(name), s"'$name'")
            case ConfigValueType.BOOLEAN =>
              // Explicitly disabled, keep in NEI if true.
              if (!value.unwrapped.asInstanceOf[Boolean]) {
                hide(stack)
              }
            case _ =>
              OpenComputers.log.error(s"Failed adding recipe for '$name', you will not be able to craft this item. The error was: Invalid value for recipe.")
              hadErrors = true
          }
        }
        else {
          OpenComputers.log.warn(s"No recipe for '$name', you will not be able to craft this item. To suppress this warning, disable the recipe (assign `false` to it).")
          hadErrors = true
        }
      }

      // Register all unknown recipes. Well. Loot disk recipes.
      if (recipes.hasPath("lootDisks")) try {
        val lootRecipes = recipes.getConfigList("lootDisks")
        val lootStacks = Loot.globalDisks.map(_._1)
        for (recipe <- lootRecipes) {
          val name = recipe.getString("name")
          lootStacks.find(s => s.getTagCompound.getString(Settings.namespace + "lootFactory") == name) match {
            case Some(stack) => addRecipe(stack, recipe, s"loot disk '$name'")
            case _ =>
              OpenComputers.log.warn(s"Failed adding recipe for loot disk '$name': No such global loot disk.")
              hadErrors = true
          }
        }
      }
      catch {
        case t: Throwable =>
          OpenComputers.log.warn("Failed parsing loot disk recipes.", t)
          hadErrors = true
      }

      if (recipes.hasPath("generic")) try {
        val genericRecipes = recipes.getConfigList("generic")
        for (recipe <- genericRecipes) {
          val result = recipe.getValue("result").unwrapped()
          parseIngredient(result) match {
            case stack: ItemStack => addRecipe(stack, recipe, s"'$result'")
            case _ =>
              OpenComputers.log.warn(s"Failed adding generic recipe for '$result': Invalid output (make sure it's not an OreDictionary name).")
              hadErrors = true
          }
        }
      }
      catch {
        case t: Throwable =>
          OpenComputers.log.warn("Failed parsing generic recipes.", t)
          hadErrors = true
      }

      // Recrafting operations.
      val accessPoint = api.Items.get(Constants.BlockName.AccessPoint)
      val cable = api.Items.get(Constants.BlockName.Cable)
      val chamelium = api.Items.get(Constants.ItemName.Chamelium)
      val chameliumBlock = api.Items.get(Constants.BlockName.ChameliumBlock)
      val drone = api.Items.get(Constants.ItemName.Drone)
      val eeprom = api.Items.get(Constants.ItemName.EEPROM)
      val floppy = api.Items.get(Constants.ItemName.Floppy)
      val hoverBoots = api.Items.get(Constants.ItemName.HoverBoots)
      val lootDisk = api.Items.get(Constants.ItemName.LootDisk)
      val mcu = api.Items.get(Constants.BlockName.Microcontroller)
      val navigationUpgrade = api.Items.get(Constants.ItemName.NavigationUpgrade)
      val print = api.Items.get(Constants.BlockName.Print)
      val relay = api.Items.get(Constants.BlockName.Relay)
      val robot = api.Items.get(Constants.BlockName.Robot)
      val switch = api.Items.get(Constants.BlockName.Switch)
      val tablet = api.Items.get(Constants.ItemName.Tablet)

      // Navigation upgrade recrafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        navigationUpgrade.createItemStack(1),
        navigationUpgrade.createItemStack(1), new ItemStack(net.minecraft.init.Items.filled_map, 1, OreDictionary.WILDCARD_VALUE)))

      // Floppy disk coloring.
      for (dye <- Color.dyes) {
        val result = floppy.createItemStack(1)
        val tag = new NBTTagCompound()
        tag.setInteger(Settings.namespace + "color", Color.dyes.indexOf(dye))
        result.setTagCompound(tag)
        GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(result, floppy.createItemStack(1), dye))
      }

      // Microcontroller recrafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        mcu.createItemStack(1),
        mcu.createItemStack(1), eeprom.createItemStack(1)))

      // Drone recrafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        drone.createItemStack(1),
        drone.createItemStack(1), eeprom.createItemStack(1)))

      // EEPROM copying via crafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        eeprom.createItemStack(2),
        eeprom.createItemStack(1), eeprom.createItemStack(1)))

      // Robot recrafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        robot.createItemStack(1),
        robot.createItemStack(1), eeprom.createItemStack(1)))

      // Tablet recrafting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        tablet.createItemStack(1),
        tablet.createItemStack(1), eeprom.createItemStack(1)))

      // Chamelium block splitting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        chamelium.createItemStack(9),
        chameliumBlock.createItemStack(1)))

      // Chamelium dying.
      for ((dye, meta) <- Color.dyes.zipWithIndex) {
        val result = chameliumBlock.createItemStack(1)
        result.setItemDamage(meta)
        val input = chameliumBlock.createItemStack(1)
        input.setItemDamage(OreDictionary.WILDCARD_VALUE)
        GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
          result,
          input, dye))
      }

      // Print beaconification.
      val beaconPrint = print.createItemStack(1)

      {
        val printData = new PrintData(beaconPrint)
        printData.isBeaconBase = true
        printData.save(beaconPrint)
      }

      for (block <- Array(
        net.minecraft.init.Blocks.iron_block,
        net.minecraft.init.Blocks.gold_block,
        net.minecraft.init.Blocks.emerald_block,
        net.minecraft.init.Blocks.diamond_block
      )) {
        GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
          beaconPrint,
          print.createItemStack(1), new ItemStack(block)))
      }

      // Floppy disk formatting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(floppy.createItemStack(1), floppy.createItemStack(1)))
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(floppy.createItemStack(1), lootDisk.createItemStack(1)))

      // Hard disk formatting.
      val hdds = Array(
        api.Items.get(Constants.ItemName.HDDTier1),
        api.Items.get(Constants.ItemName.HDDTier2),
        api.Items.get(Constants.ItemName.HDDTier3)
      )
      for (hdd <- hdds) {
        GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(hdd.createItemStack(1), hdd.createItemStack(1)))
      }

      // EEPROM formatting.
      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(eeprom.createItemStack(1), eeprom.createItemStack(1)))

      // Print light value increments.
      val lightPrint = print.createItemStack(1)

      {
        val printData = new PrintData(lightPrint)
        printData.lightLevel = 1
        printData.save(lightPrint)
      }

      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        lightPrint,
        print.createItemStack(1), new ItemStack(net.minecraft.init.Items.glowstone_dust)))

      {
        val printData = new PrintData(lightPrint)
        printData.lightLevel = 4
        printData.save(lightPrint)
      }

      GameRegistry.addRecipe(new ExtendedShapelessOreRecipe(
        lightPrint,
        print.createItemStack(1), new ItemStack(net.minecraft.init.Blocks.glowstone)))

      // Switch/AccessPoint -> Relay conversion
      GameRegistry.addShapelessRecipe(relay.createItemStack(1), accessPoint.createItemStack(1))
      GameRegistry.addShapelessRecipe(relay.createItemStack(1), switch.createItemStack(1))

      // Hover Boot dyeing
      GameRegistry.addRecipe(new ColorizeRecipe(hoverBoots.item()))
      GameRegistry.addRecipe(new DecolorizeRecipe(hoverBoots.item()))

      // Cable dyeing
      GameRegistry.addRecipe(new ColorizeRecipe(cable.block()))
      GameRegistry.addRecipe(new DecolorizeRecipe(cable.block()))

      // Loot disk cycling.
      if (Settings.get.lootRecrafting) {
        GameRegistry.addRecipe(new LootDiskCyclingRecipe())
      }
    }
    catch {
      case e: Throwable => OpenComputers.log.error("Error parsing recipes, you may not be able to craft any items from this mod!", e)
    }
    list.clear()
  }

  private def addRecipe(output: ItemStack, recipe: Config, name: String) = try {
    val recipeType = tryGetType(recipe)
    recipeHandlers.get(recipeType) match {
      case Some(recipeHandler) => recipeHandler(output, recipe)
      case _ =>
        OpenComputers.log.error(s"Failed adding recipe for $name, you will not be able to craft this item. The error was: Invalid recipe type '$recipeType'.")
        hadErrors = true
    }
  }
  catch {
    case e: RecipeException =>
      OpenComputers.log.error(s"Failed adding recipe for $name, you will not be able to craft this item.", e)
      hadErrors = true
  }

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

  def parseIngredient(entry: AnyRef): 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(s"Invalid name in recipe (not a string: $other).")
        }
      }
      else if (map.contains("item")) {
        map.get("item") match {
          case name: String =>
            findItem(name) match {
              case Some(item: Item) => new ItemStack(item, 1, tryGetId(map))
              case _ => throw new RecipeException(s"No item found with name '$name'.")
            }
          case id: Number => new ItemStack(validateItemId(id), 1, tryGetId(map))
          case other => throw new RecipeException(s"Invalid item name in recipe (not a string: $other).")
        }
      }
      else if (map.contains("block")) {
        map.get("block") match {
          case name: String =>
            findBlock(name) match {
              case Some(block: Block) => new ItemStack(block, 1, tryGetId(map))
              case _ => throw new RecipeException(s"No block found with name '$name'.")
            }
          case id: Number => new ItemStack(validateBlockId(id), 1, tryGetId(map))
          case other => throw new RecipeException(s"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 {
        findItem(name) match {
          case Some(item: Item) => new ItemStack(item, 1, 0)
          case _ =>
            findBlock(name) match {
              case Some(block: Block) => new ItemStack(block, 1, 0)
              case _ => throw new RecipeException(s"No ore dictionary entry, item or block found for ingredient with name '$name'.")
            }
        }
      }
    case other => throw new RecipeException(s"Invalid ingredient type (not a map or string): $other")
  }

  def parseFluidIngredient(entry: Config): Option[FluidStack] = {
    val fluid = FluidRegistry.getFluid(entry.getString("name"))
    val amount =
      if (entry.hasPath("amount")) entry.getInt("amount")
      else 1000
    Option(new FluidStack(fluid, amount))
  }

  private def findItem(name: String) = getObjectWithoutFallback(Item.itemRegistry, name).orElse(Item.itemRegistry.find {
    case item: Item => item.getUnlocalizedName == name || item.getUnlocalizedName == "item." + name
    case _ => false
  })

  private def findBlock(name: String) = getObjectWithoutFallback(Block.blockRegistry, name).orElse(Block.blockRegistry.find {
    case block: Block => block.getUnlocalizedName == name || block.getUnlocalizedName == "tile." + name
    case _ => false
  })

  private def getObjectWithoutFallback(registry: RegistryNamespaced, key: String) =
    if (registry.containsKey(key)) Option(registry.getObject(key))
    else None

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

  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
    val block = Block.getBlockById(index)
    if (block == null) throw new RecipeException(s"Invalid block ID: $index")
    block
  }

  private def validateItemId(id: Number) = {
    val index = id.intValue
    val item = Item.getItemById(index)
    if (item == null) throw new RecipeException(s"Invalid item ID: $index")
    item
  }

  private def hide(value: ItemStack) {
    Delegator.subItem(value) match {
      case Some(stack) => stack.showInItemList = false
      case _ => value.getItem match {
        case itemBlock: ItemBlock => itemBlock.field_150939_a match {
          case simple: SimpleBlock =>
            simple.setCreativeTab(null)
            NEI.hide(simple)
          case _ =>
        }
        case _ =>
      }
    }
  }

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

}
