package prc.utils;

import prc.autodoc.*;
import prc.autodoc.Main.TLKStore;
import prc.autodoc.Main.TwoDAStore;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static prc.Main.verbose;


/**
 * A little tool that parses des_crft_scroll, extracts unique item resrefs from it and
 * makes a merchant selling those resrefs.
 *
 * @author Heikki 'Ornedan' Aitakangas
 */
public class ScrollMerchantGen {

    /**
     * Ye olde maine methode.
     *
     * @param args The arguments
     * @throws IOException If the writing fails, just die on the exception
     */
    public static void main(String[] args) throws IOException {
        if (args.length == 0) readMe();
        String twoDAPath = null;
        String tlkPath = null;

        // parse args
        for (String param : args) {//2dadir tlkdir | [--help]
            // Parameter parseage
            if (param.startsWith("-")) {
                if (param.equals("--help")) readMe();
                else {
                    for (char c : param.substring(1).toCharArray()) {
                        switch (c) {
                            default:
                                System.out.println("Unknown parameter: " + c);
                                readMe();
                        }
                    }
                }
            } else {
                // It's a pathname
                if (twoDAPath == null)
                    twoDAPath = param;
                else if (tlkPath == null)
                    tlkPath = param;
                else {
                    System.out.println("Unknown parameter: " + param);
                    readMe();
                }
            }
        }

        // Load data
        TwoDAStore twoDA = new TwoDAStore(twoDAPath);
        TLKStore tlks = new TLKStore("dialog.tlk", "prc_consortium.tlk", tlkPath);

        doScrollMerchantGen(twoDA, tlks, "scrolltemp");
    }
    public static Set<Integer> getAllSpellCastingClasses(TwoDAStore twoDA)
    {
        Set<Integer> vanillaSpellcasterClasses = Set.of(1,2,3,6,7,9,10);
        HashSet<Integer> result = new HashSet<Integer>();
        Data_2da classes = twoDA.get("classes");
        for(int i = 0; i < classes.getEntryCount(); i++) {
            // handle the vanilla spellcasters
            if (vanillaSpellcasterClasses.contains(i)) {
                result.add(i);
                continue;
            }
            
            boolean isSpellCaster = classes.getEntry("SpellCaster", i).equals("1");
            boolean isPlayerClass = classes.getEntry("PlayerClass", i).equals("1");
            if (!isSpellCaster || !isPlayerClass) continue;

            // Attempt to load the spellbook 2da for the class. If it fails, not a spellcaster,
            // or a spellcaster that reuses another book (so we don't need a shop for them).
            try {
                String classAbrev = classes.getEntry("FeatsTable", i).toLowerCase().substring(9);
                twoDA.get("cls_spcr_" + classAbrev);
            } catch (TwoDAReadException e) {
                continue;
            }

            String label = classes.getEntry("Label", i);
            if (!label.equals("****")) result.add(i);
        }
        return result;
    }
    /**
     * Performs the scroll merchant generation for all scroll shops.
     * @param twoDA A TwoDAStore for loading 2da data from
     * @param tlks  A TLKStore for reading tlk data from
     * @throws IOException Just tossed back up
     */
    public static void doScrollMerchantGen(TwoDAStore twoDA, TLKStore tlks, String outPath) throws IOException {
        String shopPrefix = "prcSS_";
        List<ScrollInfo> allScrolls = getAllScrolls(twoDA, tlks);
        doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, "prc_scrolls", scroll -> true);
        for(int classId : getAllSpellCastingClasses(twoDA)) {
            String allClassSpellsShop = shopPrefix + "C" + classId;


            doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, allClassSpellsShop, 
                scroll -> scroll.classesForSpellWithLevel.containsKey(classId));

            for(int level = 0; level <= 9; level++) {
                final int spellLevel = level;
                String shopName = shopPrefix + "C" + classId + "L" + level;
                doScrollMerchantGenShop(twoDA, tlks, outPath, false, allScrolls, shopName, 
                    scroll -> {
                        Map<Integer, Integer> classSpellInfo = scroll.classesForSpellWithLevel;
                        return classSpellInfo.containsKey(classId) && classSpellInfo.get(classId).equals(spellLevel);
                    });
            }
        }
        char[] letters = "abcdefghijklmnopqrstuvwxyz".toCharArray();
        for(char letter : letters) {
            String shopName = "prcSS_alpha_" + letter;
            doScrollMerchantGenShop(twoDA, tlks, outPath, true, allScrolls, shopName,
                scroll -> scroll.Name.toLowerCase().startsWith("" + letter));

        }
    }


    private static List<ScrollInfo> getAllScrolls(TwoDAStore twoDA, TLKStore tlks)
    {
        Data_2da spells2da = twoDA.get("spells");
        Data_2da scrolls2da = twoDA.get("des_crft_scroll");
        // Loop over the scroll entries and get a list of unique resrefs
        TreeMap<Integer, TreeMap<String, ScrollInfo>> arcaneScrollResRefs = new TreeMap<Integer, TreeMap<String, ScrollInfo>>();
        TreeMap<Integer, TreeMap<String, ScrollInfo>> divineScrollResRefs = new TreeMap<Integer, TreeMap<String, ScrollInfo>>();
        String entry;
        for (int i = 0; i < scrolls2da.getEntryCount(); i++) {
            // Skip subradials
            if (spells2da.getEntry("Master", i).equals("****")) {
                if (!(entry = scrolls2da.getEntry("Wiz_Sorc", i)).equals("****"))
                    addScroll(arcaneScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
                if (!(entry = scrolls2da.getEntry("Cleric", i)).equals("****"))
                    addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
                if (!(entry = scrolls2da.getEntry("Paladin", i)).equals("****"))
                    addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
                if (!(entry = scrolls2da.getEntry("Druid", i)).equals("****"))
                    addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
                if (!(entry = scrolls2da.getEntry("Ranger", i)).equals("****"))
                    addScroll(divineScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
                if (!(entry = scrolls2da.getEntry("Bard", i)).equals("****"))
                    addScroll(arcaneScrollResRefs, twoDA, tlks, i, entry.toLowerCase());
            }
        }
        //int posCounter = 0;
        Stream<ScrollInfo> arcaneLevelScrollRefRefs = arcaneScrollResRefs.values()
            .stream().flatMap(scrollMap -> scrollMap.values().stream());
        Stream<ScrollInfo> divineLevelScrollRefRefs = divineScrollResRefs.values()
            .stream().flatMap(scrollMap -> scrollMap.values().stream());
        Stream<ScrollInfo> allScrollInfos = Stream.concat(arcaneLevelScrollRefRefs, divineLevelScrollRefRefs)
            .distinct()
            .sorted((scroll1, scroll2) -> -scroll1.Name.compareTo(scroll2.Name));

        return allScrollInfos.collect(Collectors.toList());

    }

    /**
     * Performs the scroll merchant generation. Made public for the purposes of BuildScrollHack.
     *
     * @param twoDA A TwoDAStore for loading 2da data from
     * @param tlks  A TLKStore for reading tlk data from
     * @param allScrolls A list containing ScrollInfo for all scrolls
     * @param shopName The name of the shop (will generate a <shopName>.utm.xml file)
     * @param scrollFilter A predicate used to filter the scrolls for the given shop
     * @throws IOException Just tossed back up
     */
    private static void doScrollMerchantGenShop(TwoDAStore twoDA, TLKStore tlks, String outPath,
            boolean generateEmptyShops,
            List<ScrollInfo> allScrolls, String shopName, Predicate<ScrollInfo> scrollFilter) throws IOException {
        // Load the 2da file
        Data_2da scrolls2da = twoDA.get("des_crft_scroll");
        Data_2da spells2da = twoDA.get("spells");



        String xmlPrefix =
                "<gff name=\"" + shopName + ".utm\" type=\"UTM \" version=\"V3.2\" >" + "\n" +
                        "    <struct id=\"-1\" >" + "\n" +
                        "        <element name=\"ResRef\" type=\"11\" value=\"" + shopName + "\" />" + "\n" +
                        "        <element name=\"LocName\" type=\"12\" value=\"64113\" >" + "\n" +
                        "            <localString languageId=\"0\" value=\"" + shopName + "\" />" + "\n" +
                        "        </element>" + "\n" +
                        "        <element name=\"Tag\" type=\"10\" value=\"" + shopName + "\" />" + "\n" +
                        "        <element name=\"MarkUp\" type=\"5\" value=\"100\" />" + "\n" +
                        "        <element name=\"MarkDown\" type=\"5\" value=\"100\" />" + "\n" +
                        "        <element name=\"BlackMarket\" type=\"0\" value=\"0\" />" + "\n" +
                        "        <element name=\"BM_MarkDown\" type=\"5\" value=\"15\" />" + "\n" +
                        "        <element name=\"IdentifyPrice\" type=\"5\" value=\"-1\" />" + "\n" +
                        "        <element name=\"MaxBuyPrice\" type=\"5\" value=\"-1\" />" + "\n" +
                        "        <element name=\"StoreGold\" type=\"5\" value=\"-1\" />" + "\n" +
                        "        <element name=\"OnOpenStore\" type=\"11\" value=\"\" />" + "\n" +
                        "        <element name=\"OnStoreClosed\" type=\"11\" value=\"\" />" + "\n" +
                        "        <element name=\"WillNotBuy\" type=\"15\" />" + "\n" +
                        "        <element name=\"WillOnlyBuy\" type=\"15\" >" + "\n" +
                        "            <struct id=\"97869\" >" + "\n" +
                        "                <element name=\"BaseItem\" type=\"5\" value=\"29\" />" + "\n" +
                        "            </struct>" + "\n" +
                        "        </element>" + "\n" +
                        "        <element name=\"StoreList\" type=\"15\" >" + "\n" +
                        "            <struct id=\"0\" />" + "\n" +
                        "            <struct id=\"4\" />" + "\n" +
                        "            <struct id=\"2\" >" + "\n" +
                        "                <element name=\"ItemList\" type=\"15\" >" + "\n";
        String xmlSuffix =
                "                </element>" + "\n" +
                        "			 </struct>" + "\n" +
                        "            <struct id=\"3\" />" + "\n" +
                        "            <struct id=\"1\" />" + "\n" +
                        "        </element>" + "\n" +
                        "        <element name=\"ID\" type=\"0\" value=\"5\" />" + "\n" +
                        "        <element name=\"Comment\" type=\"10\" value=\"\" />" + "\n" +
                        "    </struct>" + "\n" +
                        "</gff>" + "\n";

        StringBuffer xmlString = new StringBuffer();

        Stream<ScrollInfo> allScrollRefRefs = allScrolls.stream().filter(scrollFilter);
        int[] posCounterRef = new int[] { 0 };
        allScrollRefRefs.forEach(scroll -> {
            int posCounter = posCounterRef[0];
            String resref = scroll.ScrollResRef;
            String name = scroll.Name;
            xmlString.append(
                    "                    <struct id=\"" + posCounter + "\" >" + "\n" +
                            "                        <element name=\"InventoryRes\" type=\"11\" value=\"" + resref + "\" />" + "<!-- " + name + " -->" + "\n" +
                            "                        <element name=\"Repos_PosX\" type=\"2\" value=\"" + (posCounter % 10) + "\" />" + "\n" +
                            "                        <element name=\"Repos_Posy\" type=\"2\" value=\"" + (posCounter / 10) + "\" />" + "\n" +
                            "                        <element name=\"Infinite\" type=\"0\" value=\"1\" />" + "\n" +
                            "                    </struct>" + "\n"
            );
            posCounterRef[0]+= 1;

        });
        if (posCounterRef[0] == 0) {
            System.out.println("No scrolls found for shop " + shopName);
            if (!generateEmptyShops) return;
        }

        // // Then divine scrolls
        // for (Map<String, String> levelScrollResRefs : divineScrollResRefs.values())
        //     for (String name : levelScrollResRefs.keySet()) {
        //         String resref = levelScrollResRefs.get(name);
        //         xmlString.append(
        //                 "                    <struct id=\"" + posCounter + "\" >" + "\n" +
        //                         "                        <element name=\"InventoryRes\" type=\"11\" value=\"" + resref + "\" />" + "<!-- " + name + " -->" + "\n" +
        //                         "                        <element name=\"Repos_PosX\" type=\"2\" value=\"" + (posCounter % 10) + "\" />" + "\n" +
        //                         "                        <element name=\"Repos_Posy\" type=\"2\" value=\"" + (posCounter / 10) + "\" />" + "\n" +
        //                         "                        <element name=\"Infinite\" type=\"0\" value=\"1\" />" + "\n" +
        //                         "                    </struct>" + "\n"
        //         );
        //         posCounter++;
        //     }

        String shopFilePath = outPath + File.separator + shopName + ".utm.xml";
        File target = new File(shopFilePath);
        // Clean up old version if necessary
        if (target.exists()) {
            if (verbose) System.out.println("Deleting previous version of " + target.getName());
            target.delete();
        }
        if (verbose) System.out.println("Writing brand new version of " + target.getName());
        target.createNewFile();

        // Creater the writer and print
        FileWriter writer = new FileWriter(target, true);
        writer.write(xmlPrefix + xmlString.toString() + xmlSuffix);
        // Clean up
        writer.flush();
        writer.close();
    }

    private static class ScrollInfo {
        private TwoDAStore twoDAStore;

        public ScrollInfo(TwoDAStore twoDA, int rowNum, String scrollResRef, String name) {
            this.twoDAStore = twoDA;
            this.ScrollResRef = scrollResRef;
            this.Name = name;

            Data_2da spells2da = twoDA.get("spells");

            
            try {
                this.SpellLevel = Integer.parseInt(spells2da.getEntry("Innate", rowNum));
            } catch (NumberFormatException e) {
                System.err.println("Non-number value in spells.2da Innate column on line " + rowNum + ": " + spells2da.getEntry("Innate", rowNum));
                return;
            }

            Data_2da classes = twoDA.get("classes");
            this.classesForSpellWithLevel = ScrollGen.getClassesForSpell(twoDA, rowNum);

        }
        public int SpellLevel = -1;
        public String ScrollResRef;
        public String Name;
        public Map<Integer, Integer> classesForSpellWithLevel;

        public boolean equals(Object other)
        {
            if (this == other) return true;
            if (other == null) return false;
            if (getClass() != other.getClass()) return false;
            ScrollInfo otherScroll = (ScrollInfo)other;
            return ScrollResRef.equals(otherScroll.ScrollResRef);
        }
        public int hashCode() {
            return Objects.hash(ScrollResRef);
        }
    }

    private static void addScroll(TreeMap<Integer, TreeMap<String, ScrollInfo>> scrollResRefs,
                                  TwoDAStore twoDAStore, TLKStore tlks, int rowNum, String scrollResRef) {
        int innateLevel = -1,
                tlkRef = -1;

        Data_2da spells2da = twoDAStore.get("spells");
        // HACK - Skip non-PRC scrolls
        if (!scrollResRef.startsWith("prc_scr_"))
            return;


        try {
            tlkRef = Integer.parseInt(spells2da.getEntry("Name", rowNum));
        } catch (NumberFormatException e) {
            System.err.println("Non-number value in spells.2da Name column on line " + rowNum + ": " + spells2da.getEntry("Name", rowNum));
            return;
        }
        ScrollInfo scroll = new ScrollInfo(twoDAStore, rowNum, scrollResRef, tlks.get(tlkRef));
        scroll.SpellLevel = innateLevel;

        innateLevel = scroll.SpellLevel;

        if (scrollResRefs.get(innateLevel) == null)
            scrollResRefs.put(innateLevel, new TreeMap<String, ScrollInfo>());

        scrollResRefs.get(innateLevel).put(tlks.get(tlkRef), scroll);
    }

    /**
     * Prints the use instructions for this program and kills execution.
     */
    private static void readMe() {
        //                  0        1         2         3         4         5         6         7         8
        //					12345678901234567890123456789012345678901234567890123456789012345678901234567890
        System.out.println("Usage:\n" +
                "  java -jar prc.jar scrmrchgen 2dadir tlkdir | [--help]\n" +
                "\n" +
                "2dadir   Path to a directory containing des_crft_scroll.2da and spells.2da.\n" +
                "tlkdir   Path to a directory containing dialog.tlk and prc_consortium.tlk\n" +
                "\n" +
                "--help      prints this info you are reading\n" +
                "\n" +
                "\n" +
                "Generates a merchant file - prc_scrolls.utm - based on the given scroll list\n" +
                "2da file. The merchant file will be written to current directory in Pspeed's\n" +
                "XML <-> Gff -xml format\n"
        );
        System.exit(0);
    }
}