#!/usr/bin/python3

from Utils import output_path
import argparse, os.path, json, sys, shutil, random, copy, requests

from rando.RandoSettings import RandoSettings, GraphSettings
from rando.RandoExec import RandoExec
from graph.graph_utils import vanillaTransitions, vanillaBossesTransitions, GraphUtils, getAccessPoint
from utils.parameters import Knows, Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, diff2text, appDir
from rom.rom_patches import RomPatches
from rom.rompatcher import RomPatcher
from utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir
from utils.version import displayedVersion
from logic.smbool import SMBool
from utils.doorsmanager import DoorsManager
from logic.logic import Logic

import utils.log
from worlds.sm.Options import StartLocation

# we need to know the logic before doing anything else
def getLogic():
    # check if --logic is there
    logic = 'vanilla'
    for i, param in enumerate(sys.argv):
        if param == '--logic' and i+1 < len(sys.argv):
            logic = sys.argv[i+1]
    return logic
Logic.factory(getLogic())
defaultMultiValues = getDefaultMultiValues()
speeds = defaultMultiValues['progressionSpeed']
energyQties = defaultMultiValues['energyQty']
progDiffs = defaultMultiValues['progressionDifficulty']
morphPlacements = defaultMultiValues['morphPlacement']
majorsSplits = defaultMultiValues['majorsSplit']
gravityBehaviours = defaultMultiValues['gravityBehaviour']

def randomMulti(args, param, defaultMultiValues):
    value = args[param]

    isRandom = False
    if value == "random":
        isRandom = True
        if args[param+"List"] != None:
            # use provided list
            choices = args[param+"List"].split(',')
            value = random.choice(choices)
        else:
            # use default list
            value = random.choice(defaultMultiValues)

    return (isRandom, value)

def dumpErrorMsg(outFileName, msg):
    print("DIAG: " + msg)
    if outFileName is not None:
        with open(outFileName, 'w') as jsonFile:
            json.dump({"errorMsg": msg}, jsonFile)

def dumpErrorMsgs(outFileName, msgs):
    dumpErrorMsg(outFileName, joinErrorMsgs(msgs))

def joinErrorMsgs(msgs):
    return '\n'.join(msgs)

def restricted_float(x):
    x = float(x)
    if x < 0.0 or x > 9.0:
        raise argparse.ArgumentTypeError("%r not in range [1.0, 9.0]"%(x,))
    return x

def to_pascal_case_with_space(snake_str):
    return snake_str.replace("_", " ").title()

class VariaRandomizer:

    parser = argparse.ArgumentParser(description="Random Metroid Randomizer")
    parser.add_argument('--patchOnly',
                        help="only apply patches, do not perform any randomization", action='store_true',
                        dest='patchOnly', default=False)
    parser.add_argument('--param', '-p', help="the input parameters",
                        default=None, dest='paramsFileName')
    parser.add_argument('--dir',
                        help="output directory for ROM and dot files",
                        dest='directory', nargs='?', default='.')
    parser.add_argument('--dot',
                        help="generate dot file with area graph",
                        action='store_true',dest='dot', default=False)
    parser.add_argument('--area', help="area mode",
                        dest='area', nargs='?', const=True, default=False)
    parser.add_argument('--areaLayoutBase',
                        help="use simple layout patch for area mode", action='store_true',
                        dest='areaLayoutBase', default=False)
    parser.add_argument('--lightArea', help="keep number of transitions between vanilla areas", action='store_true',
                        dest='lightArea', default=False)
    parser.add_argument('--escapeRando',
                        help="Randomize the escape sequence",
                        dest='escapeRando', nargs='?', const=True, default=False)
    parser.add_argument('--noRemoveEscapeEnemies',
                        help="Do not remove enemies during escape sequence", action='store_true',
                        dest='noRemoveEscapeEnemies', default=False)
    parser.add_argument('--bosses', help="randomize bosses",
                        dest='bosses', nargs='?', const=True, default=False)
    parser.add_argument('--minimizer', help="minimizer mode: area and boss mixed together. arg is number of non boss locations",
                        dest='minimizerN', nargs='?', const=35, default=None,
                        choices=[str(i) for i in range(30,101)]+["random"])
    parser.add_argument('--minimizerTourian',
                        help="Tourian speedup in minimizer mode",
                        dest='minimizerTourian', nargs='?', const=True, default=False)
    parser.add_argument('--startLocation', help="Name of the Access Point to start from",
                        dest='startLocation', nargs='?', default="Landing Site",
                        choices=['random'] + GraphUtils.getStartAccessPointNames())
    parser.add_argument('--startLocationList', help="list to choose from when random",
                        dest='startLocationList', nargs='?', default=None)
    parser.add_argument('--debug', '-d', help="activate debug logging", dest='debug',
                        action='store_true')
    parser.add_argument('--maxDifficulty', '-t',
                        help="the maximum difficulty generated seed will be for given parameters",
                        dest='maxDifficulty', nargs='?', default=None,
                        choices=['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania', 'random'])
    parser.add_argument('--minDifficulty',
                        help="the minimum difficulty generated seed will be for given parameters (speedrun prog speed required)",
                        dest='minDifficulty', nargs='?', default=None,
                        choices=['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania'])
    parser.add_argument('--seed', '-s', help="randomization seed to use", dest='seed',
                        nargs='?', default=0, type=int)
    parser.add_argument('--rom', '-r',
                        help="the vanilla ROM",
                        dest='rom', nargs='?', default=None)
    parser.add_argument('--output',
                        help="to choose the name of the generated json (for the webservice)",
                        dest='output', nargs='?', default=None)
    parser.add_argument('--preset',
                        help="the name of the preset (for the webservice)",
                        dest='preset', nargs='?', default=None)
    parser.add_argument('--patch', '-c',
                        help="optional patches to add",
                        dest='patches', nargs='?', default=[], action='append',
                        choices=['itemsounds.ips', 'elevators_doors_speed.ips', 'random_music.ips',
                                'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips',
                                'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump',
                                'refill_before_save.ips', 'remove_elevators_doors_speed.ips',
                                'remove_itemsounds.ips', 'vanilla_music.ips'])
    parser.add_argument('--missileQty', '-m',
                        help="quantity of missiles",
                        dest='missileQty', nargs='?', default=3,
                        type=restricted_float)
    parser.add_argument('--superQty', '-q',
                        help="quantity of super missiles",
                        dest='superQty', nargs='?', default=2,
                        type=restricted_float)
    parser.add_argument('--powerBombQty', '-w',
                        help="quantity of power bombs",
                        dest='powerBombQty', nargs='?', default=1,
                        type=restricted_float)
    parser.add_argument('--minorQty', '-n',
                        help="quantity of minors",
                        dest='minorQty', nargs='?', default=100,
                        choices=[str(i) for i in range(0,101)])
    parser.add_argument('--energyQty', '-g',
                        help="quantity of ETanks/Reserve Tanks",
                        dest='energyQty', nargs='?', default='vanilla',
                        choices=energyQties + ['random'])
    parser.add_argument('--energyQtyList', help="list to choose from when random",
                        dest='energyQtyList', nargs='?', default=None)
    parser.add_argument('--strictMinors',
                        help="minors quantities values will be strictly followed instead of being probabilities",
                        dest='strictMinors', nargs='?', const=True, default=False)
    parser.add_argument('--majorsSplit',
                        help="how to split majors/minors: Full, FullWithHUD, Major, Chozo, Scavenger",
                        dest='majorsSplit', nargs='?', choices=majorsSplits + ['random'], default='Full')
    parser.add_argument('--majorsSplitList', help="list to choose from when random",
                        dest='majorsSplitList', nargs='?', default=None)
    parser.add_argument('--scavNumLocs',
                        help="For Scavenger split, number of major locations in the mandatory route",
                        dest='scavNumLocs', nargs='?', default=10,
                        choices=["0"]+[str(i) for i in range(4,17)])
    parser.add_argument('--scavRandomized',
                        help="For Scavenger split, decide whether mandatory major locs will have non-vanilla items",
                        dest='scavRandomized', nargs='?', const=True, default=False)
    parser.add_argument('--scavEscape',
                        help="For Scavenger split, decide whether escape sequence shall be triggered as soon as the hunt is over",
                        dest='scavEscape', nargs='?', const=True, default=False)
    parser.add_argument('--suitsRestriction',
                        help="no suits in early game",
                        dest='suitsRestriction', nargs='?', const=True, default=False)
    parser.add_argument('--morphPlacement',
                        help="morph placement",
                        dest='morphPlacement', nargs='?', default='early',
                        choices=morphPlacements + ['random'])
    parser.add_argument('--morphPlacementList', help="list to choose from when random",
                        dest='morphPlacementList', nargs='?', default=None)
    parser.add_argument('--hideItems', help="Like in dessy's rando hide half of the items",
                        dest="hideItems", nargs='?', const=True, default=False)
    parser.add_argument('--progressionSpeed', '-i',
                        help="progression speed, from " + str(speeds) + ". 'random' picks a random speed from these. Pick a random speed from a subset using comma-separated values, like 'slow,medium,fast'.",
                        dest='progressionSpeed', nargs='?', default='medium', choices=speeds+['random'])
    parser.add_argument('--progressionSpeedList', help="list to choose from when random",
                        dest='progressionSpeedList', nargs='?', default=None)
    parser.add_argument('--progressionDifficulty',
                        help="",
                        dest='progressionDifficulty', nargs='?', default='normal',
                        choices=progDiffs + ['random'])
    parser.add_argument('--progressionDifficultyList', help="list to choose from when random",
                        dest='progressionDifficultyList', nargs='?', default=None)
    parser.add_argument('--superFun',
                        help="randomly remove major items from the pool for maximum enjoyment",
                        dest='superFun', nargs='?', default=[], action='append',
                        choices=['Movement', 'Combat', 'Suits', 'MovementRandom', 'CombatRandom', 'SuitsRandom'])
    parser.add_argument('--animals',
                        help="randomly change the save the animals room",
                        dest='animals', action='store_true', default=False)
    parser.add_argument('--nolayout',
                        help="do not include total randomizer layout patches",
                        dest='noLayout', action='store_true', default=False)
    parser.add_argument('--gravityBehaviour',
                        help="varia/gravity suits behaviour",
                        dest='gravityBehaviour', nargs='?', default='Balanced', choices=gravityBehaviours+['random'])
    parser.add_argument('--gravityBehaviourList', help="list to choose from when random",
                        dest='gravityBehaviourList', nargs='?', default=None)
    parser.add_argument('--nerfedCharge',
                        help="apply nerfed charge patch",
                        dest='nerfedCharge', action='store_true', default=False)
    parser.add_argument('--novariatweaks',
                        help="do not include VARIA randomizer tweaks",
                        dest='noVariaTweaks', action='store_true', default=False)
    parser.add_argument('--controls',
                        help="specify controls, comma-separated, in that order: Shoot,Jump,Dash,ItemSelect,ItemCancel,AngleUp,AngleDown. Possible values: A,B,X,Y,L,R,Select,None",
                        dest='controls')
    parser.add_argument('--moonwalk',
                        help="Enables moonwalk by default",
                        dest='moonWalk', action='store_true', default=False)
    parser.add_argument('--runtime',
                        help="Maximum runtime limit in seconds. If 0 or negative, no runtime limit. Default is 30.",
                        dest='runtimeLimit_s', nargs='?', default=30, type=int)
    parser.add_argument('--race', help="Race mode magic number, between 1 and 65535", dest='raceMagic',
                        type=int)
    parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true')
    parser.add_argument('--palette', help="Randomize the palettes", dest='palette', action='store_true')
    parser.add_argument('--individual_suit_shift', help="palette param", action='store_true',
                        dest='individual_suit_shift', default=False)
    parser.add_argument('--individual_tileset_shift', help="palette param", action='store_true',
                        dest='individual_tileset_shift', default=False)
    parser.add_argument('--no_match_ship_and_power', help="palette param", action='store_false',
                        dest='match_ship_and_power', default=True)
    parser.add_argument('--seperate_enemy_palette_groups', help="palette param", action='store_true',
                        dest='seperate_enemy_palette_groups', default=False)
    parser.add_argument('--no_match_room_shift_with_boss', help="palette param", action='store_false',
                        dest='match_room_shift_with_boss', default=True)
    parser.add_argument('--no_shift_tileset_palette', help="palette param", action='store_false',
                        dest='shift_tileset_palette', default=True)
    parser.add_argument('--no_shift_boss_palettes', help="palette param", action='store_false',
                        dest='shift_boss_palettes', default=True)
    parser.add_argument('--no_shift_suit_palettes', help="palette param", action='store_false',
                        dest='shift_suit_palettes', default=True)
    parser.add_argument('--no_shift_enemy_palettes', help="palette param", action='store_false',
                        dest='shift_enemy_palettes', default=True)
    parser.add_argument('--no_shift_beam_palettes', help="palette param", action='store_false',
                        dest='shift_beam_palettes', default=True)
    parser.add_argument('--no_shift_ship_palette', help="palette param", action='store_false',
                        dest='shift_ship_palette', default=True)
    parser.add_argument('--min_degree', help="min hue shift", dest='min_degree', nargs='?', default=-180, type=int)
    parser.add_argument('--max_degree', help="max hue shift", dest='max_degree', nargs='?', default=180, type=int)
    parser.add_argument('--no_global_shift', help="", action='store_false', dest='global_shift', default=True)
    parser.add_argument('--invert', help="invert color range", dest='invert', action='store_true', default=False)
    parser.add_argument('--no_blue_door_palette', help="palette param", action='store_true',
                        dest='no_blue_door_palette', default=False)
    parser.add_argument('--ext_stats', help="dump extended stats SQL", nargs='?', default=None, dest='extStatsFilename')
    parser.add_argument('--randoPreset', help="rando preset file", dest="randoPreset", nargs='?', default=None)
    parser.add_argument('--fakeRandoPreset', help="for prog speed stats", dest="fakeRandoPreset", nargs='?', default=None)
    parser.add_argument('--plandoRando', help="json string with already placed items/locs", dest="plandoRando",
                        nargs='?', default=None)
    parser.add_argument('--sprite', help='use a custom sprite for Samus', dest='sprite', default=None)
    parser.add_argument('--no_spin_attack', help='when using a custom sprite, use the same animation for screw attack with or without Space Jump', dest='noSpinAttack', action='store_true', default=False)
    parser.add_argument('--customItemNames', help='add custom item names for some of them, related to the custom sprite',
                        dest='customItemNames', action='store_true', default=False)
    parser.add_argument('--ship', help='use a custom sprite for Samus ship', dest='ship', default=None)
    parser.add_argument('--seedIps', help='ips generated from previous seed', dest='seedIps', default=None)
    parser.add_argument('--jm,', help="display data used by jm for its stats", dest='jm', action='store_true', default=False)
    parser.add_argument('--doorsColorsRando', help='randomize color of colored doors', dest='doorsColorsRando',
                        nargs='?', const=True, default=False)
    parser.add_argument('--allowGreyDoors', help='add grey color in doors colors pool', dest='allowGreyDoors',
                        nargs='?', const=True, default=False)
    parser.add_argument('--logic', help='logic to use', dest='logic', nargs='?', default="varia", choices=["varia", "rotation"])
    parser.add_argument('--hud', help='Enable VARIA hud', dest='hud',
                        nargs='?', const=True, default=False)

    def __init__(self, world, rom, player):
        # parse args       
        self.args = copy.deepcopy(VariaRandomizer.parser.parse_args(["--logic", "varia"])) #dummy custom args to skip parsing _sys.argv while still get default values
        self.player = player
        args = self.args
        args.rom = rom
        # args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key)

        if args.output is None and args.rom is None:
            print("Need --output or --rom parameter")
            sys.exit(-1)
        elif args.output is not None and args.rom is not None:
            print("Can't have both --output and --rom parameters")
            sys.exit(-1)

        if args.plandoRando != None and args.output == None:
            print("plandoRando param requires output param")
            sys.exit(-1)

        utils.log.init(args.debug)
        logger = utils.log.get('Rando')

        Logic.factory(args.logic)

        # service to force an argument value and notify it
        argDict = vars(args)
        self.forcedArgs = {}
        self.optErrMsgs = [ ]
        optErrMsgs = self.optErrMsgs
        def forceArg(arg, value, msg, altValue=None, webArg=None, webValue=None):
            okValues = [value]
            if altValue is not None:
                okValues.append(altValue)

            if argDict[arg] not in okValues:
                argDict[arg] = value
                self.forcedArgs[webArg if webArg != None else arg] = webValue if webValue != None else value
                # print(msg)
                # optErrMsgs.append(msg)

        preset = loadRandoPreset(world, self.player, args)
        # use the skill preset from the rando preset
        if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None:
            args.paramsFileName = os.path.join(appDir, getPresetDir(preset), preset+".json")

        # if diff preset given, load it
        if args.paramsFileName is not None:
            PresetLoader.factory(args.paramsFileName).load(self.player)
            preset = os.path.splitext(os.path.basename(args.paramsFileName))[0]

            if args.preset is not None:
                preset = args.preset
        else:
            if preset == 'custom':
                PresetLoader.factory(world.custom_preset[player].value).load(self.player)
            elif preset == 'varia_custom':
                url = 'https://randommetroidsolver.pythonanywhere.com/presetWebService'
                preset_name = next(iter(world.varia_custom_preset[player].value))
                payload = '{{"preset": "{}"}}'.format(preset_name)
                headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
                response = requests.post(url, data=payload, headers=headers)
                if response.ok:
                    PresetLoader.factory(json.loads(response.text)).load(self.player)
                else:
                    raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name))
            else:
                preset = 'default'
                PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player)

        

        logger.debug("preset: {}".format(preset))

        # if no seed given, choose one
        if args.seed == 0:
            self.seed = random.randrange(sys.maxsize)
        else:
            self.seed = args.seed
        logger.debug("seed: {}".format(self.seed))

        if args.raceMagic is not None:
            if args.raceMagic <= 0 or args.raceMagic >= 0x10000:
                print("Invalid magic")
                sys.exit(-1)

        # if no max diff, set it very high
        if args.maxDifficulty:
            if args.maxDifficulty == 'random':
                diffs = ['easy', 'medium', 'hard', 'harder', 'hardcore', 'mania']
                self.maxDifficulty = text2diff[random.choice(diffs)]
            else:
                self.maxDifficulty = text2diff[args.maxDifficulty]
        else:
            self.maxDifficulty = infinity
        # same as solver, increase max difficulty
        threshold = self.maxDifficulty
        epsilon = 0.001
        if self.maxDifficulty <= easy:
            threshold = medium - epsilon
        elif self.maxDifficulty <= medium:
            threshold = hard - epsilon
        elif self.maxDifficulty <= hard:
            threshold = harder - epsilon
        elif self.maxDifficulty <= harder:
            threshold = hardcore - epsilon
        elif self.maxDifficulty <= hardcore:
            threshold = mania - epsilon
        maxDifficulty = threshold
        logger.debug("maxDifficulty: {}".format(self.maxDifficulty))

        # handle random parameters with dynamic pool of values
        (_, progSpeed) = randomMulti(args.__dict__, "progressionSpeed", speeds)
        (_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs)
        (majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits)
        (_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours)
        if args.minDifficulty:
            minDifficulty = text2diff[args.minDifficulty]
            if progSpeed != "speedrun":
                optErrMsgs.append("Minimum difficulty setting ignored, as prog speed is not speedrun")
        else:
            minDifficulty = 0

        if args.area == True and args.bosses == True and args.minimizerN is not None:
            forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full", altValue='FullWithHUD')
            if args.minimizerN == "random":
                self.minimizerN = random.randint(30, 60)
                logger.debug("minimizerN: {}".format(self.minimizerN))
            else:
                self.minimizerN = int(args.minimizerN)
        else:
            self.minimizerN = None
        areaRandom = False
        if args.area == 'random':
            areaRandom = True
            args.area = bool(random.getrandbits(1))
        logger.debug("area: {}".format(args.area))

        doorsColorsRandom = False
        if args.doorsColorsRando == 'random':
            doorsColorsRandom = True
            args.doorsColorsRando = bool(random.getrandbits(1))
        logger.debug("doorsColorsRando: {}".format(args.doorsColorsRando))

        bossesRandom = False
        if args.bosses == 'random':
            bossesRandom = True
            args.bosses = bool(random.getrandbits(1))
        logger.debug("bosses: {}".format(args.bosses))

        if args.escapeRando == 'random':
            args.escapeRando = bool(random.getrandbits(1))
        logger.debug("escapeRando: {}".format(args.escapeRando))

        if args.suitsRestriction != False and self.minimizerN is not None:
            forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')

        if args.suitsRestriction == 'random':
            if args.morphPlacement == 'late' and args.area == True:
                forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
            else:
                args.suitsRestriction = bool(random.getrandbits(1))
        logger.debug("suitsRestriction: {}".format(args.suitsRestriction))

        if args.hideItems == 'random':
            args.hideItems = bool(random.getrandbits(1))

        if args.morphPlacement == 'random':
            if args.morphPlacementList != None:
                morphPlacements = args.morphPlacementList.split(',')
            args.morphPlacement = random.choice(morphPlacements)
        # Scavenger Hunt constraints
        if args.majorsSplit == 'Scavenger':
            forceArg('progressionSpeed', 'speedrun', "'Progression speed' forced to speedrun")
            progSpeed = "speedrun"
            forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
            if not GraphUtils.isStandardStart(args.startLocation):
                forceArg('startLocation', "Landing Site", "Start Location forced to Landing Site because of Scavenger mode")
            if args.morphPlacement == 'late':
                forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late")
            if args.scavEscape == True:
                forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on')
                forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off')
        # random fill makes certain options unavailable
        if (progSpeed == 'speedrun' or progSpeed == 'basic') and args.majorsSplit != 'Scavenger':
            forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
            progDiff = args.progressionDifficulty
        logger.debug("progressionDifficulty: {}".format(progDiff))

        if args.strictMinors == 'random':
            args.strictMinors = bool(random.getrandbits(1))

        # in plando rando we know that the start ap is ok
        if not GraphUtils.isStandardStart(args.startLocation) and args.plandoRando is None:
            if args.majorsSplit in ['Major', "Chozo"]:
                forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on')
            forceArg('noVariaTweaks', False, "'VARIA tweaks' forced to on", webValue='on')
            forceArg('noLayout', False, "'Anti-softlock layout patches' forced to on", webValue='on')
            forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off')
            forceArg('areaLayoutBase', False, "'Additional layout patches for easier navigation' forced to on", webValue='on')
            possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(args.area, self.maxDifficulty, args.morphPlacement, self.player)
            if args.startLocation == 'random':
                if args.startLocationList != None:
                    # to be able to give the list in jm we had to replace ' ' with '_', do the opposite operation
                    startLocationList = args.startLocationList.replace('_', ' ')
                    startLocationList = startLocationList.split(',')
                    # intersection between user whishes and reality
                    possibleStartAPs = sorted(list(set(possibleStartAPs).intersection(set(startLocationList))))
                    if len(possibleStartAPs) == 0:
                        optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList]
                        optErrMsgs.append('Invalid start locations list with your settings.')
                        dumpErrorMsgs(args.output, optErrMsgs)
                        sys.exit(-1)
                args.startLocation = random.choice(possibleStartAPs)
            elif args.startLocation not in possibleStartAPs:
                args.startLocation = 'Landing Site'
                world.start_location[player] = StartLocation(StartLocation.default)
                #optErrMsgs.append('Invalid start location: {}.  {}'.format(args.startLocation, reasons[args.startLocation]))
                #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
                #dumpErrorMsgs(args.output, optErrMsgs)
                #sys.exit(-1)
        ap = getAccessPoint(args.startLocation)
        if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
            forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
        else:
            if progSpeed == 'speedrun':
                if args.morphPlacement == 'late':
                    forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late")
                elif (not GraphUtils.isStandardStart(args.startLocation)) and args.morphPlacement != 'normal':
                    forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for custom start location")
            if args.majorsSplit == 'Chozo' and args.morphPlacement == "late":
                forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for Chozo")
        #if args.patchOnly == False:
        #    print("SEED: " + str(self.seed))

        # fill restrictions dict
        restrictions = { 'Suits' : args.suitsRestriction, 'Morph' : args.morphPlacement, "doors": "normal" if not args.doorsColorsRando else "late" }
        restrictions['MajorMinor'] = 'Full' if args.majorsSplit == 'FullWithHUD' else args.majorsSplit
        if restrictions["MajorMinor"] == "Scavenger":
            scavNumLocs = int(args.scavNumLocs)
            if scavNumLocs == 0:
                scavNumLocs = random.randint(4,16)
            restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized, 'escape': args.scavEscape}
        seedCode = 'X'
        if majorsSplitRandom == False:
            if restrictions['MajorMinor'] == 'Full':
                seedCode = 'FX'
            elif restrictions['MajorMinor'] == 'Chozo':
                seedCode = 'ZX'
            elif restrictions['MajorMinor'] == 'Major':
                seedCode = 'MX'
            elif restrictions['MajorMinor'] == 'Scavenger':
                seedCode = 'SX'
        if args.bosses == True and bossesRandom == False:
            seedCode = 'B'+seedCode
        if args.doorsColorsRando == True and doorsColorsRandom == False:
            seedCode = 'D'+seedCode
        if args.area == True and areaRandom == False:
            seedCode = 'A'+seedCode

        # output ROM name
        #if args.patchOnly == False:
        #    self.fileName = 'VARIA_Randomizer_' + seedCode + str(self.seed) + '_' + preset
        #    if args.progressionSpeed != "random":
        #        self.fileName += "_" + args.progressionSpeed
        #else:
        #    self.fileName = 'VARIA' # TODO : find better way to name the file (argument?)
        self.fileName = output_path
        seedName = self.fileName
        if args.directory != '.':
            self.fileName = args.directory + '/' + self.fileName
        if args.noLayout == True:
            RomPatches.ActivePatches[self.player] = RomPatches.TotalBase.copy()
        else:
            RomPatches.ActivePatches[self.player] = RomPatches.Total.copy()
        RomPatches.ActivePatches[self.player].remove(RomPatches.BlueBrinstarBlueDoor)
        RomPatches.ActivePatches[self.player] += GraphUtils.getGraphPatches(args.startLocation)
        if self.gravityBehaviour != "Balanced":
            RomPatches.ActivePatches[self.player].remove(RomPatches.NoGravityEnvProtection)
        if self.gravityBehaviour == "Progressive":
            RomPatches.ActivePatches[self.player].append(RomPatches.ProgressiveSuits)
        if args.nerfedCharge == True:
            RomPatches.ActivePatches[self.player].append(RomPatches.NerfedCharge)
        if args.noVariaTweaks == False:
            RomPatches.ActivePatches[self.player] += RomPatches.VariaTweaks
        if self.minimizerN is not None:
            RomPatches.ActivePatches[self.player].append(RomPatches.NoGadoras)
            if args.minimizerTourian == True:
                RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian
        missileQty = float(args.missileQty)
        superQty = float(args.superQty)
        powerBombQty = float(args.powerBombQty)
        minorQty = int(args.minorQty)
        self.energyQty = args.energyQty
        if missileQty < 1:
            missileQty = random.randint(1, 9)
        if superQty < 1:
            superQty = random.randint(1, 9)
        if powerBombQty < 1:
            powerBombQty = random.randint(1, 9)
        if minorQty < 1:
            minorQty = random.randint(25, 100)
        if self.energyQty == 'random':
            if args.energyQtyList != None:
                # with jm can't have a list with space in it
                energyQtyList = args.energyQtyList.replace('_', ' ')
                energyQties = energyQtyList.split(',')
            self.energyQty = random.choice(energyQties)
        if self.energyQty == 'ultra sparse':
            # add nerfed rainbow beam patch
            RomPatches.ActivePatches[self.player].append(RomPatches.NerfedRainbowBeam)
        qty = {'energy': self.energyQty,
            'minors': minorQty,
            'ammo': { 'Missile': missileQty,
                        'Super': superQty,
                        'PowerBomb': powerBombQty },
            'strictMinors' : args.strictMinors }
        logger.debug("quantities: {}".format(qty))

        if len(args.superFun) > 0:
            superFun = []
            for fun in args.superFun:
                if fun.find('Random') != -1:
                    if bool(random.getrandbits(1)) == True:
                        superFun.append(fun[0:fun.find('Random')])
                else:
                    superFun.append(fun)
            args.superFun = superFun
        logger.debug("superFun: {}".format(args.superFun))
  
        ctrlButton = ["A", "B", "X", "Y", "L", "R", "Select"]
        ctrl = Controller.ControllerDict[self.player]
        self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton }
        args.moonWalk = ctrl.Moonwalk

        plandoSettings = None
        if args.plandoRando is not None:
            forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun")
            progSpeed = 'speedrun'
            forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full")
            forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal")
            forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal")
            progDiff = 'normal'
            args.plandoRando = json.loads(args.plandoRando)
            RomPatches.ActivePatches[self.player] = args.plandoRando["patches"]
            DoorsManager.unserialize(args.plandoRando["doors"])
            plandoSettings = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']}
        randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty,
                                    restrictions, args.superFun, args.runtimeLimit_s,
                                    plandoSettings, minDifficulty)

        # print some parameters for jm's stats
        if args.jm == True:
            print("startLocation:{}".format(args.startLocation))
            print("progressionSpeed:{}".format(progSpeed))
            print("majorsSplit:{}".format(args.majorsSplit))
            print("morphPlacement:{}".format(args.morphPlacement))

        dotFile = None
        if args.area == True:
            if args.dot == True:
                dotFile = args.directory + '/' + seedName + '.dot'
            RomPatches.ActivePatches[self.player] += RomPatches.AreaBaseSet
            if args.areaLayoutBase == False:
                RomPatches.ActivePatches[self.player] += RomPatches.AreaComfortSet
        if args.doorsColorsRando == True:
            RomPatches.ActivePatches[self.player].append(RomPatches.RedDoorsMissileOnly)
        graphSettings = GraphSettings(args.startLocation, args.area, args.lightArea, args.bosses,
                                    args.escapeRando, self.minimizerN, dotFile, args.doorsColorsRando, args.allowGreyDoors,
                                    args.plandoRando["transitions"] if args.plandoRando != None else None)

        if args.plandoRando is None:
            DoorsManager.setDoorsColor(self.player)

        self.escapeAttr = None
        if args.patchOnly == False:
            try:
                self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player)
                self.container = self.randoExec.randomize()
                # if we couldn't find an area layout then the escape graph is not created either
                # and getDoorConnections will crash if random escape is activated.
                stuck = False
                if not stuck or args.vcr == True:
                    self.doors = GraphUtils.getDoorConnections(self.randoExec.areaGraph,
                                                        args.area, args.bosses,
                                                        args.escapeRando)
                    escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None
                    if escapeAttr is not None:
                        escapeAttr['patches'] = []
                        if args.noRemoveEscapeEnemies == True:
                            escapeAttr['patches'].append("Escape_Rando_Enable_Enemies")
                        if args.scavEscape == True:
                            escapeAttr['patches'].append('Escape_Scavenger')
            except Exception as e:
                import traceback
                traceback.print_exc(file=sys.stdout)
                dumpErrorMsg(args.output, "Error: {}".format(e))
                sys.exit(-1)
        else:
            stuck = False
            itemLocs = []
            progItemLocs = None
        if stuck == True:
            dumpErrorMsg(args.output, self.randoExec.errorMsg)
            print("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg))
            # in vcr mode we still want the seed to be generated to analyze it
            if args.vcr == False:
                sys.exit(-1)
        #if args.patchOnly == False:
        #    randoExec.postProcessItemLocs(itemLocs, args.hideItems)

    def PatchRom(self, outputFilename, customPatchApply = None):
        args = self.args
        optErrMsgs = self.optErrMsgs

        # choose on animal patch
        if args.animals == True:
            animalsPatches = ['animal_enemies.ips', 'animals.ips', 'draygonimals.ips', 'escapimals.ips',
                            'gameend.ips', 'grey_door_animals.ips', 'low_timer.ips', 'metalimals.ips',
                            'phantoonimals.ips', 'ridleyimals.ips']
            if args.escapeRando == False:
                args.patches.append(random.choice(animalsPatches))
                args.patches.append("Escape_Animals_Change_Event")
            else:
                optErrMsgs.append("Ignored animals surprise because of escape randomization")
        # # transform itemLocs in our usual dict(location, item), exclude minors, we'll get them with the solver
        # locsItems = {}
        # for itemLoc in itemLocs:
        #     locName = itemLoc.Location.Name
        #     itemType = itemLoc.Item.Type
        #     if itemType in ['Missile', 'Super', 'PowerBomb']:
        #         continue
        #     locsItems[locName] = itemType
        # if args.debug == True:
        #     for loc in sorted(locsItems.keys()):
        #         print('{:>50}: {:>16} '.format(loc, locsItems[loc]))

        # if args.plandoRando != None:
        #     with open(args.output, 'w') as jsonFile:
        #         json.dump({"itemLocs": [il.json() for il in itemLocs], "errorMsg": randoExec.errorMsg}, jsonFile)
        #     sys.exit(0)

        # # generate extended stats
        # if args.extStatsFilename != None:
        #     with open(args.extStatsFilename, 'a') as extStatsFile:
        #         skillPreset = os.path.splitext(os.path.basename(args.paramsFileName))[0]
        #         if args.fakeRandoPreset is not None:
        #             randoPreset = args.fakeRandoPreset
        #         else:
        #             randoPreset = os.path.splitext(os.path.basename(args.randoPreset))[0]
        #         db.DB.dumpExtStatsItems(skillPreset, randoPreset, locsItems, extStatsFile)

        try:
            # args.rom is not None: generate local rom named filename.sfc with args.rom as source
            # args.output is not None: generate local json named args.output
            if args.rom is not None:
                # patch local rom
                romFileName = args.rom
                shutil.copyfile(romFileName, outputFilename)
                romPatcher = RomPatcher(outputFilename, args.raceMagic, False, self.player)
            else:
                romPatcher = RomPatcher(magic=args.raceMagic)

            if args.hud == True or args.majorsSplit == "FullWithHUD":
                args.patches.append("varia_hud.ips")
            if args.patchOnly == False:
                romPatcher.applyIPSPatches(args.startLocation, args.patches,
                                        args.noLayout, self.gravityBehaviour,
                                        args.area, args.bosses, args.areaLayoutBase,
                                        args.noVariaTweaks, args.nerfedCharge, self.energyQty == 'ultra sparse',
                                        self.escapeAttr, self.minimizerN, args.minimizerTourian,
                                        args.doorsColorsRando)
            else:
                 # from customizer permalink, apply previously generated seed ips first
                if args.seedIps != None:
                    romPatcher.applyIPSPatch(args.seedIps)

                romPatcher.addIPSPatches(args.patches)
                # don't color randomize custom ships
                args.shift_ship_palette = False

            if customPatchApply != None:
                customPatchApply(romPatcher)

            # we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits),
            # but in web mode we only want to generate a global ips at the end
#            if args.rom != None:
#                romPatcher.commitIPS()
            if args.patchOnly == False:
#                romPatcher.writeItemsLocs(itemLocs)
#                romPatcher.writeSplitLocs(args.majorsSplit, itemLocs, progItemLocs)
                 romPatcher.writeItemsNumber()
                 romPatcher.writeSeed(self.seed) # lol if race mode
#                romPatcher.writeSpoiler(itemLocs, progItemLocs)
#                romPatcher.writeRandoSettings(self.randoExec.randoSettings, itemLocs)
                 romPatcher.writeDoorConnections(self.doors)
                 romPatcher.writeVersion(displayedVersion)
            if self.ctrlDict is not None:
                romPatcher.writeControls(self.ctrlDict)
            if args.moonWalk == True:
                romPatcher.enableMoonWalk()
            if args.patchOnly == False:
                romPatcher.writeMagic()
                romPatcher.writeMajorsSplit(args.majorsSplit)
            # if args.palette == True:
            #     paletteSettings = {
            #         "global_shift": None,
            #         "individual_suit_shift": None,
            #         "individual_tileset_shift": None,
            #         "match_ship_and_power": None,
            #         "seperate_enemy_palette_groups": None,
            #         "match_room_shift_with_boss": None,
            #         "shift_tileset_palette": None,
            #         "shift_boss_palettes": None,
            #         "shift_suit_palettes": None,
            #         "shift_enemy_palettes": None,
            #         "shift_beam_palettes": None,
            #         "shift_ship_palette": None,
            #         "min_degree": None,
            #         "max_degree": None,
            #         "invert": None,
            #         "no_blue_door_palette": None
            #     }
            #     for param in paletteSettings:
            #         paletteSettings[param] = getattr(args, param)
            #     PaletteRando(romPatcher, paletteSettings, args.sprite).randomize()

            # web mode, generate only one ips at the end
            if args.rom == None:
                romPatcher.commitIPS()
            romPatcher.end()
            if args.patchOnly == False:
                if len(optErrMsgs) > 0:
#                    optErrMsgs.append(randoExec.errorMsg)
                    msg = joinErrorMsgs(optErrMsgs)
                else:
#                    msg = randoExec.errorMsg
                    msg = ''
            else:
                msg = ''
            if args.rom is None: # web mode
                data = romPatcher.romFile.data
                self.fileName = '{}.sfc'.format(self.fileName)
                data["fileName"] = self.fileName
                # error msg in json to be displayed by the web site
                data["errorMsg"] = msg
                # replaced parameters to update stats in database
                if len(self.forcedArgs) > 0:
                    data["forcedArgs"] = self.forcedArgs
                with open(outputFilename, 'w') as jsonFile:
                    json.dump(data, jsonFile)
            else: # CLI mode
                if msg != "":
                    print(msg)
        except Exception as e:
            import traceback
            traceback.print_exc(file=sys.stdout)
            msg = "Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e)
            dumpErrorMsg(args.output, msg)
            sys.exit(-1)

#        if stuck == True:
#            print("Rom generated for debug purpose: {}".format(self.fileName))
#        else:
        # print("Rom generated: {}".format(self.fileName))