Mapeate.Mapas = function(mapeate, entradas){

    const self = this, 
          tipos = {}, 
          mapas = {}, 
          cache = {}, 
          claves = ["x", "y", "ancho", "alto"], 
          tamano_cache = {x : 0, y : 0};
    let iniciado = false, 
        sobreescribir_por_defecto, 
        sobreescribir_tipos_por_defecto, 
        mapas_hilo_de_proceso, 
        fecha_inicio_juego;

    let selector = this.selector;
    let global = this.global;

    const constructor = () => {

        mapeate.print("info", "mapeate_mapas_construyendose");

        selector = self.selector = new Mapeate.Mapas.Selector(mapeate, entradas);
        global = self.global = new Mapeate.Mapas.Global(mapeate, entradas);

        mapeate.print("ok", "mapeate_mapas_construido");
    
    };

    this.iniciar = callback => {

        const terminar = estado => typeof callback == "function" && callback(estado);

        mapeate.print("info", "mapeate_mapas_iniciando");

        if(iniciado){
            mapeate.print("warn", "mapeate_mapas_ya_iniciado");
            terminar(false);
            return false;
        };
        iniciado = true;

        sobreescribir_por_defecto = mapeate.configuracion(["mapas_sobreescribir", "sobreescribir"]);
        sobreescribir_tipos_por_defecto = mapeate.configuracion(["mapas_tipos_sobreescribir", "mapas_sobreescribir", "sobreescribir"]);

        mapeate.ejecutar_array_asincrono([
            "archivos_de_mapas_tipos_por_defecto", 
            "archivos_de_mapas_tipos", 
            "mapas_tipos_por_defecto", 
            "mapas_tipos"
        ], (clave, callback) => self.cargar_tipos(mapeate.configuracion(clave), true, callback), () => {
            mapeate.ejecutar_array_asincrono([
                "archivos_de_mapas_por_defecto", 
                "archivos_de_mapas", 
                "mapas_por_defecto", 
                "mapas"
            ], (clave, callback) => self.cargar(mapeate.configuracion(clave), true, callback), () => {
                mapeate.ejecutar_array_asincrono([
                    selector, global
                ], (elemento, callback) => elemento.iniciar(callback), () => {
                    mapeate.print("ok", "mapeate_mapas_iniciado");
                    mapas_hilo_de_proceso = mapeate.anadir_hilo_de_proceso(hilo_de_proceso_metodo);
                    terminar(true);
                });
            });
        });

        return true;
    };

    const hilo_de_proceso_metodo = () => {
        if(!mapeate.si_mismo)
            return;

        const mapa = mapeate.si_mismo.querySelector(".juego>.mapa"), 
              svg = mapa.querySelector("svg");

        if(svg){

            const limites = {}, 
                  final = {}, 
                  tiempo = Date.now() - fecha_inicio_juego;

            [limites.x, limites.y, limites.ancho, limites.alto] = svg.getAttribute("data-caja").split(" ").map(valor => Number(valor));
            [final.x, final.y, final.ancho, final.alto] = mapeate.porcentuar(
                limites.ancho, limites.alto, mapa.offsetWidth, mapa.offsetHeight
            );
                  

            if(tamano_cache.x != final.ancho || tamano_cache.y != final.alto){
                
                const proporcion = final.ancho / limites.ancho;

                // console.log([proporcion, Object.values(limites).map(valor => valor * proporcion >> 0).join(" ")]);
                
                // svg.currentScale = proporcion;
                svg.setAttribute("viewBox", Object.values(limites).join(" "));
                svg.setAttribute("width", final.ancho);
                svg.setAttribute("height", final.alto);
                // svg.style.width = ancho + "px";
                // svg.style.height = alto + "px";
                svg.style.marginLeft = -(final.ancho / 2) + "px";
                svg.style.marginTop = -(final.alto / 2) + "px";
                
                tamano_cache.x = final.ancho;
                tamano_cache.y = final.alto;

            };

            mapeate.si_mismo.querySelector(".juego .temporizador>section").innerText = (
                ("00" + (tiempo / 3600000 >> 0)).slice(-2) + ":" + 
                ("00" + (tiempo / 60000 % 60 >> 0)).slice(-2) + ":" + 
                ("00" + (tiempo / 1000 % 60 >> 0)).slice(-2) + "." + 
                ("00" + (tiempo % 1000 >> 0)).slice(-3)
            );

        };
        
    };

    const cargar_tipos = (clave, callback) => mapeate.leer_archivo(tipos[clave].url, datos => {
        if(datos){
            tipos[clave].datos = datos;
            tipos[clave].ok = true;
        };
        mapeate.ejecutar(callback);
    });

    this.cargar_tipos = (entradas, sobreescribir, callback) => {
        typeof sobreescribir == "boolean" || (sobreescribir = sobreescribir_tipos_por_defecto);
        mapeate.cargar_diccionarios(mapeate.coger_array(entradas), (nuevos_tipos, callback) => {

            let i = 0;
            const l = Object.keys(nuevos_tipos).length, 
                  terminar = () => ++ i == l && mapeate.ejecutar(callback);

            for(const clave in nuevos_tipos){
                if(sobreescribir || !tipos[clave]){
                    tipos[clave] = {
                        url : nuevos_tipos[clave].url, 
                        clase : nuevos_tipos[clave].clase, 
                        niveles_ocultables : nuevos_tipos[clave].niveles_ocultables, 
                        datos : null, 
                        ok : false
                    };
                    cargar_tipos(clave, terminar);
                }else 
                    terminar();
            };

        }, callback, 0);

    };

    this.cargar = (entradas, sobreescribir, callback) => {
        typeof sobreescribir == "boolean" || (sobreescribir = sobreescribir_por_defecto);
        mapeate.cargar_diccionarios(mapeate.coger_array(entradas), (nuevos_mapas, callback) => {
            for(const clave in nuevos_mapas)
                (sobreescribir || !mapas[clave]) && 
                nuevos_mapas[clave] && 
                typeof nuevos_mapas[clave] == "object" && 
                (mapas[clave] = {
                    ...nuevos_mapas[clave], 
                    ...(nuevos_mapas[clave].tipo && tipos[nuevos_mapas[clave].tipo] ? tipos[nuevos_mapas[clave].tipo] : {})
                });
            mapeate.ejecutar(callback);
        }, callback, 0);
    };

    this.coger = claves => {

        const l = (claves = mapeate.coger_array_de_strings(claves)).length;

        for(let i = 0; i < l; i ++)
            if(mapas[claves[i]] !== undefined)
                return mapas[claves[i]];
        return claves[0] || null;
    };

    this.coger_todos = () => ({...mapas});

    this.cache = (clave, datos) => datos === undefined ? cache[clave] : (cache[clave] = datos);

    this.construir = (clave, niveles) => {

        let error = (
            clave === undefined ? 1 << 0 : 
            clave === null ? 1 << 1 : 
            typeof clave != "string" ? 1 << 2 : 
            !clave ? 1 << 3 : 
            !(clave = clave.trim()) ? 1 << 4 : 
            mapas[clave] === undefined ? 1 << 5 : 
            mapas[clave] === null ? 1 << 6 : 
        0) << 1;

        if(error)
            return;

        const mapa = {
            nombre : clave, 
            ...mapas[clave], 
            datos : cache[mapas[clave].url]
        };
        let objetivos = "", seleccion;

        (seleccion = [].concat(...mapa.elementos.filter((bloque, i) => niveles.includes(i)))).sort(() => .5 - Math.random()).forEach((elemento, i) => {

            const datos_comunes = {
                elemento : elemento, 
                formato : mapa.formato, 
                seleccionado : !i
            };

            objetivos += mapeate.string_variables(mapeate.string_variables(mapeate.vistas.coger("elemento_juego_seleccion", datos_comunes), datos_comunes));
            
        });

        mapeate.si_mismo.querySelector(".juego .objetivos ul").innerHTML = objetivos;
        mapeate.si_mismo.querySelector(".juego .puntos section").innerText = 0;

        if(!mapa.datos)
            mapeate.leer_archivo(mapas[clave].url, datos => {

                mapas[clave].clase && 
                self[mapas[clave].clase] && 
                self[mapas[clave].clase].preparar && 
                (datos = self[mapas[clave].clase].preparar(datos));

                cache[mapas[clave].url] = datos;
                mapa.datos = datos;

                construir(mapa, niveles, seleccion);

            });
        else 
            construir(mapa, niveles, seleccion);

    };

    const construir = (datos, niveles, seleccion) => {

        const juego = mapeate.si_mismo.querySelector(".juego"), 
              mapa = juego.querySelector(".mapa section"), 
              menu = mapeate.si_mismo.querySelector(".juego .objetivos ul");

        mapa.innerHTML = (`
            <div class="` + datos.nombre + `" data-clase="` + datos.clase + `"` + [0, 1, 2, 3].map(i => (
                ` data-nivel-` + i + `-ocultable="` + datos.niveles_ocultables.includes(i) + `"` + 
                ` data-nivel-` + i + `="` + niveles.includes(i) + `"`
            )).join("") + `>` + datos.datos + `</div>
        `);

        mapeate.si_mismo.querySelector(".menu-mapas").setAttribute("data-visible", false);
        mapeate.si_mismo.querySelector(".juego").setAttribute("data-visible", true);

        mapeate.precargar("." + datos.nombre + ">svg", svg => {

            const limites = svg.getBBox();

            svg.setAttribute("data-ancho", limites.width);
            svg.setAttribute("data-alto", limites.height);
            svg.setAttribute("data-caja", datos.posicion.join(" "));

            tamano_cache.x = 0;
            tamano_cache.y = 0;

            svg.querySelectorAll("[data-habilitado]").forEach(elemento => {

                let clon = elemento;
                const clave = elemento.getAttribute("data-objetivo");

                while(clon && clon.hasAttribute){
                    if(clon.tagName.toLowerCase() == "svg")
                        break;

                    const objetivo = clon.getAttribute("data-objetivo");

                    if(objetivo != clave && seleccion.includes(objetivo)){

                        const opcion = menu.querySelector("[data-clave=" + clave + "]");

                        opcion && opcion.remove();

                        return;
                    };
                    clon = clon.parentNode;
                };

                elemento.setAttribute("data-habilitado", seleccion.includes(elemento.getAttribute("data-objetivo")))

            });
            mapeate.si_mismo.querySelectorAll(".juego fieldset:not(.mapa)").forEach(campo => campo.innerHTML += mapeate.vistas.coger("posicionador"));

            fecha_inicio_juego = Date.now();

        });

    };

    this.volver_al_menu = (elemento, evento) => {

        mapeate.si_mismo.querySelector(".menu-mapas").setAttribute("data-visible", true);
        mapeate.si_mismo.querySelector(".juego").setAttribute("data-visible", false);

    };

    this.mostrar_seleccion = (elemento, evento) => mapeate.si_mismo.querySelector(".juego .elemento section").innerText = (
        elemento.getAttribute("data-habilitado") == "false" ? "" : 
        elemento.getAttribute("data-acertado") == "false" ? "??????" : 
        elemento.getAttribute("data-title")
    );

    this.ocultar_seleccion = (elemento, evento) => mapeate.si_mismo.querySelector(".juego .elemento section").innerText = "";

    this.elegir = (elemento, evento) => {
        if(elemento.getAttribute("data-habilitado") == "false")
            return;

        const clave = elemento.getAttribute("data-objetivo"), 
              seleccionado = mapeate.si_mismo.querySelector(".juego .objetivos [data-seleccionado=true][data-ok=false]");

        if(!seleccionado)
            mapeate.alerta("mapeate_mapas_seleccionar_objetivo");
        else if(seleccionado.getAttribute("data-clave") == clave){

            const opciones = [...seleccionado.parentNode.querySelectorAll("li")];
            let siguiente = null, 
                preparado = false;

            [0, 1].some(() => opciones.some(nuevo => {
                if(preparado){
                    if(nuevo.getAttribute("data-seleccionado") == "false" && nuevo.getAttribute("data-ok") == "false"){
                        siguiente = nuevo;
                        return true;
                    };
                }else if(nuevo.getAttribute("data-clave") == clave)
                    preparado = true;
            }));

            seleccionado.setAttribute("data-seleccionado", false);
            seleccionado.setAttribute("data-ok", true);
            elemento.setAttribute("data-acertado", true);
            with(mapeate.si_mismo.querySelector(".juego .puntos section"))innerText = Number(innerText) + 5;

            if(siguiente){
                siguiente.setAttribute("data-seleccionado", true);
                seleccionado.parentNode.parentNode.scrollTop = siguiente.offsetTop - (siguiente.offsetHeight / 2) - (siguiente.parentNode.parentNode.offsetHeight / 2);
            }else 
                mapeate.alerta("mapeate_mapas_ganaste", () => self.volver_al_menu(elemento, evento));

        }else{
            with(mapeate.si_mismo.querySelector(".juego .puntos section"))innerText = Number(innerText) - 1;
            mapeate.alerta("mapeate_mapas_mal_elegido");
        };

    };

    this.seleccionar_objetivo = (elemento, evento) => {
        if(elemento.getAttribute("data-seleccionado") == "true" || elemento.getAttribute("data-ok") == "true")
            return;

        mapeate.si_mismo.querySelectorAll(".juego .objetivos li[data-seleccionado=true]").forEach(elemento => elemento.setAttribute("data-seleccionado", false));
        elemento.setAttribute("data-seleccionado", true);

    };
 
    constructor();

};