package client;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import lib.data.MapleData;
import lib.data.MapleDataDirectoryEntry;
import lib.data.MapleDataFileEntry;
import lib.data.MapleDataProvider;
import lib.data.MapleDataProviderFactory;
import lib.data.MapleDataTool;
import tools.Randomizer;
import tools.StringUtil;
import tools.Triple;

public class SkillFactory {

    private static final Map<Integer, Skill> skills = new HashMap<>();
    private static final Map<String, Integer> delays = new HashMap<>();
    private static final Map<Integer, CraftingEntry> crafts = new HashMap<>();
    private static final Map<Integer, FamiliarEntry> familiars = new HashMap<>();
    private static final Map<Integer, List<Integer>> skillsByJob = new HashMap<>();
    private static final Map<Integer, SummonSkillEntry> SummonSkillInformation = new HashMap<>();
    
    public static void load() {
        final MapleData delayData = MapleDataProviderFactory.getDataProvider("Character.wz").getData("00002000.img");
        
        final MapleData stringData = MapleDataProviderFactory.getDataProvider("String.wz").getData("Skill.img");
        
        final MapleDataProvider data = MapleDataProviderFactory.getDataProvider("Skill.wz");
        final MapleDataDirectoryEntry root = data.getRoot();
        
        int del = 0; //buster is 67 but its the 57th one!
        for (MapleData delay : delayData) {
            if (!delay.getName().equals("info")) {
                delays.put(delay.getName(), del);
                del++;
            }
        }

        Pattern num = Pattern.compile("^(0|[0-9]*)$");
        for (MapleDataFileEntry file : root.getFiles()) {
        	String img = file.getName().replace(".img", "");
        	Matcher match = num.matcher(img);
        	
        	if (match.find()) {
                for (MapleData sData : data.getData(file.getName()).getChildByPath("skill")) {
                	int skillid = Integer.parseInt(sData.getName());
                	int jobid = (skillid / 10000);
                    Skill skill = Skill.loadFromData(skillid, sData, delayData);
                    
                    List<Integer> sArray = skillsByJob.get(jobid);
                    if (sArray == null) {
                    	sArray = new ArrayList<>();
                    	skillsByJob.put(jobid, sArray);
                    }
                    sArray.add(skillid);

                    String name = getName(skillid, stringData);  
                    skill.setName(name);

                    skills.put(skillid, skill);

                    addSummonData(sData, skillid);
                }	
            }
        }
        
        data.getData("FamiliarSkill.img").forEach(img -> addFamiliarData(img));
        
        for(byte b = 0; b <= 4; b++) {
        	data.getData("Recipe_920"+b+".img").forEach(img -> addRecipeData(img));
        }
    }
    
    private static void addSummonData(MapleData data, int skillid) {
    	MapleData summon_data = data.getChildByPath("summon/attack1/info");
        if (summon_data != null) {

            SummonSkillEntry entry = new SummonSkillEntry();
            entry.type = (byte) MapleDataTool.getInt("type", summon_data, 0);
            if (skillid == 33101008) {
            	entry.mobCount = 3;
            } else {
            	entry.mobCount = ((byte) MapleDataTool.getInt("mobCount", summon_data, 1));
            }
            entry.attackCount = (byte) MapleDataTool.getInt("attackCount", summon_data, 1);
            entry.targetPlus = (byte) MapleDataTool.getInt("targetPlus", summon_data, 1);
            if (summon_data.getChildByPath("range/lt") != null) {
                final MapleData ltd = summon_data.getChildByPath("range/lt");
                entry.lt = (Point) ltd.getData();
                entry.rb = (Point) summon_data.getChildByPath("range/rb").getData();
            } else {
                entry.lt = new Point(-100, -100);
                entry.rb = new Point(100, 100);
            }
            //sse.range = (short) MapleDataTool.getInt("range/r", summon_data, 0);
            entry.delay = MapleDataTool.getInt("effectAfter", summon_data, 0) + MapleDataTool.getInt("attackAfter", summon_data, 0);
            for (MapleData effect : summon_data) {
                if (effect.getChildren().size() > 0) {
                    for (final MapleData effectEntry : effect) {
                        entry.delay += MapleDataTool.getIntConvert("delay", effectEntry, 0);
                    }
                }
            }
            for (MapleData effect : data.getChildByPath("summon/attack1")) {
                entry.delay += MapleDataTool.getIntConvert("delay", effect, 0);
            }
            SummonSkillInformation.put(skillid, entry);
        }
    }
    
    private static void addFamiliarData(MapleData familiar) {
    	int skillid = Integer.parseInt(familiar.getName());
        
        FamiliarEntry skill = new FamiliarEntry();
        skill.prop = (byte) MapleDataTool.getInt("prop", familiar, 0);
        skill.time = (byte) MapleDataTool.getInt("time", familiar, 0);
        skill.attackCount = (byte) MapleDataTool.getInt("attackCount", familiar, 1);
        skill.targetCount = (byte) MapleDataTool.getInt("targetCount", familiar, 1);
        skill.speed = (byte) MapleDataTool.getInt("speed", familiar, 1);
        skill.knockback = MapleDataTool.getInt("knockback", familiar, 0) > 0 || MapleDataTool.getInt("attract", familiar, 0) > 0;
        if (familiar.getChildByPath("lt") != null) {
            skill.lt = (Point) familiar.getChildByPath("lt").getData();
            skill.rb = (Point) familiar.getChildByPath("rb").getData();
        }
        if (MapleDataTool.getInt("stun", familiar, 0) > 0) {
            skill.status.add(MonsterStatus.STUN);
        }
        if (MapleDataTool.getInt("slow", familiar, 0) > 0) {
            skill.status.add(MonsterStatus.SPEED);
        }
        familiars.put(skillid, skill);
    }
    
    private static void addRecipeData(MapleData recipe) {
    	int skillid = Integer.parseInt(recipe.getName());
        
        CraftingEntry skil = new CraftingEntry(skillid, (byte) MapleDataTool.getInt("incFatigability", recipe, 0), (byte) MapleDataTool.getInt("reqSkillLevel", recipe, 0), (byte) MapleDataTool.getInt("incSkillProficiency", recipe, 0), MapleDataTool.getInt("needOpenItem", recipe, 0) > 0, MapleDataTool.getInt("period", recipe, 0));
        for (MapleData d : recipe.getChildByPath("target")) {
            skil.targetItems.add(new Triple<>(MapleDataTool.getInt("item", d, 0), MapleDataTool.getInt("count", d, 0), MapleDataTool.getInt("probWeight", d, 0)));
        }
        for (MapleData d : recipe.getChildByPath("recipe")) {
            skil.reqItems.put(MapleDataTool.getInt("item", d, 0), MapleDataTool.getInt("count", d, 0));
        }
        crafts.put(skillid, skil);
    }

    public static List<Integer> getSkillsByJob(final int jobId) {
        return skillsByJob.get(jobId);
    }

    public static String getSkillName(final int id) {
        Skill skil = getSkill(id);
        if (skil != null) {
            return skil.getName();
        }
        return null;
    }

    public static Integer getDelay(final String id) {
        if (Delay.fromString(id) != null) {
            return Delay.fromString(id).i;
        }
        return delays.get(id);
    }

    private static String getName(final int id, final MapleData stringData) {
        String strId = Integer.toString(id);
        strId = StringUtil.getLeftPaddedStr(strId, '0', 7);
        MapleData skillroot = stringData.getChildByPath(strId);
        if (skillroot != null) {
            return MapleDataTool.getString(skillroot.getChildByPath("name"), "");
        }
        return "";
    }

    public static SummonSkillEntry getSummonData(final int skillid) {
        return SummonSkillInformation.get(skillid);
    }

    public static Collection<Skill> getAllSkills() {
        return skills.values();
    }

    public static Skill getSkill(final int id) {
        if (!skills.isEmpty()) {
            if (id >= 92000000 && id < 100000000 && crafts.containsKey(id)) { //92000000
                return crafts.get(id);
            }
            return skills.get(id);
        }
        return null;
    }

    public static long getDefaultSExpiry(final Skill skill) {
        if (skill == null) {
            return -1;
        }
        return (skill.isTimeLimited() ? (System.currentTimeMillis() + (long) (30L * 24L * 60L * 60L * 1000L)) : -1);
    }

    public static CraftingEntry getCraft(final int id) {
        if (!crafts.isEmpty()) {
            return crafts.get(Integer.valueOf(id));
        }

        return null;
    }

    public static FamiliarEntry getFamiliar(final int id) {
        if (!familiars.isEmpty()) {
            return familiars.get(Integer.valueOf(id));
        }

        return null;
    }

    public static class CraftingEntry extends Skill {
        //reqSkillProficiency -> always seems to be 0

        public boolean needOpenItem;
        public int period;
        public byte incFatigability, reqSkillLevel, incSkillProficiency;
        public List<Triple<Integer, Integer, Integer>> targetItems = new ArrayList<>(); // itemId / amount / probability
        public Map<Integer, Integer> reqItems = new HashMap<>(); // itemId / amount

        public CraftingEntry(int id, byte incFatigability, byte reqSkillLevel, byte incSkillProficiency, boolean needOpenItem, int period) {
            super(id);
            this.incFatigability = incFatigability;
            this.reqSkillLevel = reqSkillLevel;
            this.incSkillProficiency = incSkillProficiency;
            this.needOpenItem = needOpenItem;
            this.period = period;
        }
    }

    public static class FamiliarEntry {

        public byte prop, time, attackCount, targetCount, speed;
        public Point lt, rb;
        public boolean knockback;
        public EnumSet<MonsterStatus> status = EnumSet.noneOf(MonsterStatus.class);

        public final boolean makeChanceResult() {
            return prop >= 100 || Randomizer.nextInt(100) < prop;
        }
    }

    public static enum Delay {

        walk1(0x00),
        walk2(0x01),
        stand1(0x02),
        stand2(0x03),
        alert(0x04),
        swingO1(0x05),
        swingO2(0x06),
        swingO3(0x07),
        swingOF(0x08),
        swingT1(0x09),
        swingT2(0x0A),
        swingT3(0x0B),
        swingTF(0x0C),
        swingP1(0x0D),
        swingP2(0x0E),
        swingPF(0x0F),
        stabO1(0x10),
        stabO2(0x11),
        stabOF(0x12),
        stabT1(0x13),
        stabT2(0x14),
        stabTF(0x15),
        swingD1(0x16),
        swingD2(0x17),
        stabD1(0x18),
        swingDb1(0x19),
        swingDb2(0x1A),
        swingC1(0x1B),
        swingC2(0x1C),
        rushBoom(0x1C),
        tripleBlow(0x1D),
        quadBlow(0x1E),
        deathBlow(0x1F),
        finishBlow(0x20),
        finishAttack(0x21),
        finishAttack_link(0x22),
        finishAttack_link2(0x22),
        shoot1(0x23),
        shoot2(0x24),
        shootF(0x25),
        shootDb2(0x28),
        shotC1(0x29),
        dash(0x2B),
        dash2(0x2C), //hack. doesn't really exist
        proneStab(0x2F),
        prone(0x30),
        heal(0x31),
        fly(0x32),
        jump(0x33),
        sit(0x34),
        rope(0x35),
        dead(0x36),
        ladder(0x37),
        rain(0x38),
        alert2(0x40),
        alert3(0x41),
        alert4(0x42),
        alert5(0x43),
        alert6(0x44),
        alert7(0x45),
        ladder2(0x46),
        rope2(0x47),
        shoot6(0x48),
        magic1(0x49),
        magic2(0x4A),
        magic3(0x4B),
        magic5(0x4C),
        magic6(0x4D),
        explosion(0x4D),
        burster1(0x4E),
        burster2(0x4F),
        savage(0x50),
        avenger(0x51),
        assaulter(0x52),
        prone2(0x53),
        assassination(0x54),
        assassinationS(0x55),
        tornadoDash(0x58),
        tornadoDashStop(0x58),
        tornadoRush(0x58),
        rush(0x59),
        rush2(0x5A),
        brandish1(0x5B),
        brandish2(0x5C),
        braveSlash(0x5D),
        braveslash1(0x5D),
        braveslash2(0x5E),
        braveslash3(0x5F),
        braveslash4(0x60),
        darkImpale(0x61),
        sanctuary(0x62),
        meteor(0x63),
        paralyze(0x64),
        blizzard(0x65),
        genesis(0x66),
        blast(0x69),
        smokeshell(0x6A),
        showdown(0x6B),
        ninjastorm(0x6C),
        chainlightning(0x6D),
        holyshield(0x6E),
        resurrection(0x6F),
        somersault(0x70),
        straight(0x71),
        eburster(0x72),
        backspin(0x73),
        eorb(0x74),
        screw(0x75),
        doubleupper(0x76),
        dragonstrike(0x77),
        doublefire(0x78),
        triplefire(0x79),
        fake(0x7A),
        airstrike(0x7B),
        edrain(0x7C),
        octopus(0x7D),
        backstep(0x7E),
        shot(0x7F),
        rapidfire(0x7F),
        fireburner(0x81),
        coolingeffect(0x82),
        fist(0x84),
        timeleap(0x85),
        homing(0x86),
        ghostwalk(0x87),
        ghoststand(0x88),
        ghostjump(0x89),
        ghostproneStab(0x8A),
        ghostladder(0x8B),
        ghostrope(0x8C),
        ghostfly(0x8D),
        ghostsit(0x8E),
        cannon(0x8F),
        torpedo(0x90),
        darksight(0x91),
        bamboo(0x92),
        pyramid(0x93),
        wave(0x94),
        blade(0x95),
        souldriver(0x96),
        firestrike(0x97),
        flamegear(0x98),
        stormbreak(0x99),
        vampire(0x9A),
        swingT2PoleArm(0x9C),
        swingP1PoleArm(0x9D),
        swingP2PoleArm(0x9E),
        doubleSwing(0x9F),
        tripleSwing(0xA0),
        fullSwingDouble(0xA1),
        fullSwingTriple(0xA2),
        overSwingDouble(0xA3),
        overSwingTriple(0xA4),
        rollingSpin(0xA5),
        comboSmash(0xA6),
        comboFenrir(0xA7),
        comboTempest(0xA8),
        finalCharge(0xA9),
        finalBlow(0xAB),
        finalToss(0xAC),
        magicmissile(0xAD),
        lightningBolt(0xAE),
        dragonBreathe(0xAF),
        breathe_prepare(0xB0),
        dragonIceBreathe(0xB1),
        icebreathe_prepare(0xB2),
        blaze(0xB3),
        fireCircle(0xB4),
        illusion(0xB5),
        magicFlare(0xB6),
        elementalReset(0xB7),
        magicRegistance(0xB8),
        magicBooster(0xB9),
        magicShield(0xBA),
        recoveryAura(0xBB),
        flameWheel(0xBC),
        killingWing(0xBD),
        OnixBlessing(0xBE),
        Earthquake(0xBF),
        soulStone(0xC0),
        dragonThrust(0xC1),
        ghostLettering(0xC2),
        darkFog(0xC3),
        slow(0xC4),
        mapleHero(0xC5),
        Awakening(0xC6),
        flyingAssaulter(0xC7),
        tripleStab(0xC8),
        fatalBlow(0xC9),
        slashStorm1(0xCA),
        slashStorm2(0xCB),
        bloodyStorm(0xCC),
        flashBang(0xCD),
        upperStab(0xCE),
        bladeFury(0xCF),
        chainPull(0xD1),
        chainAttack(0xD1),
        owlDead(0xD2),
        monsterBombPrepare(0xD4),
        monsterBombThrow(0xD4),
        finalCut(0xD5),
        finalCutPrepare(0xD5),
        suddenRaid(0xD7), //idk, not in data anymore
        fly2(0xD8),
        fly2Move(0xD9),
        fly2Skill(0xDA),
        knockback(0xDB),
        rbooster_pre(0xDF),
        rbooster(0xDF),
        rbooster_after(0xDF),
        crossRoad(0xE2),
        nemesis(0xE3),
        tank(0xEA),
        tank_laser(0xEE),
        siege_pre(0xF0),
        tank_siegepre(0xF0), //just to make it work with the skill, these two
        sonicBoom(0xF3),
        darkLightning(0xF5),
        darkChain(0xF6),
        cyclone_pre(0),
        cyclone(0), //energy attack
        glacialchain(0xF7),
        flamethrower(0xFB),
        flamethrower_pre(0xFB),
        flamethrower2(0xFC),
        flamethrower_pre2(0xFC),
        gatlingshot(0x101),
        gatlingshot2(0x102),
        drillrush(0x103),
        earthslug(0x104),
        rpunch(0x105),
        clawCut(0x106),
        swallow(0x109),
        swallow_attack(0x109),
        swallow_loop(0x109),
        flashRain(0x111),
        OnixProtection(0x11C),
        OnixWill(0x11D),
        phantomBlow(0x11E),
        comboJudgement(0x11F),
        arrowRain(0x120),
        arrowEruption(0x121),
        iceStrike(0x122),
        swingT2Giant(0x125),
        cannonJump(0x127),
        swiftShot(0x128),
        giganticBackstep(0x12A),
        mistEruption(0x12B),
        cannonSmash(0x12C),
        cannonSlam(0x12D),
        flamesplash(0x12E),
        noiseWave(0x132),
        superCannon(0x136),
        jShot(0x138),
        demonSlasher(0x139),
        bombExplosion(0x13A),
        cannonSpike(0x13B),
        speedDualShot(0x13C),
        strikeDual(0x13D),
        bluntSmash(0x13F),
        crossPiercing(0x140),
        piercing(0x141),
        elfTornado(0x143),
        immolation(0x144),
        multiSniping(0x147),
        windEffect(0x148),
        elfrush(0x149),
        elfrush2(0x149),
        dealingRush(0x14E),
        maxForce0(0x150),
        maxForce1(0x151),
        maxForce2(0x152),
        maxForce3(0x153),
        iceAttack1(0x158),
        iceAttack2(0x159),
        iceSmash(0x15A),
        iceTempest(0x15B),
        iceChop(0x15C),
        icePanic(0x15D),
        iceDoubleJump(0x15E),
        //special: pirate morph attacks
        shockwave(0x169),
        demolition(0x16A),
        snatch(0x16B),
        windspear(0x16C),
        windshot(0x16D);
        public int i;

        private Delay(int i) {
            this.i = i;
        }

        public static Delay fromString(String s) {
            for (Delay b : Delay.values()) {
                if (b.name().equalsIgnoreCase(s)) {
                    return b;
                }
            }
            return null;
        }
    }
}
