// License: GPL. For details, see LICENSE file.
/**
 * Extracts tag information for the taginfo project.
 *
 * Run from the base directory of a JOSM checkout:
 *
 * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t mappaint
 * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t presets
 * groovy -cp dist/josm-custom.jar scripts/taginfoextract.groovy -t external_presets
 */
import java.awt.image.BufferedImage
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

import javax.imageio.ImageIO
import javax.json.Json
import javax.json.stream.JsonGenerator

import org.openstreetmap.josm.Main
import org.openstreetmap.josm.actions.DeleteAction
import org.openstreetmap.josm.command.DeleteCommand
import org.openstreetmap.josm.data.Version
import org.openstreetmap.josm.data.coor.LatLon
import org.openstreetmap.josm.data.osm.Node
import org.openstreetmap.josm.data.osm.OsmPrimitive
import org.openstreetmap.josm.data.osm.Way
import org.openstreetmap.josm.data.osm.visitor.paint.MapPaintSettings
import org.openstreetmap.josm.data.osm.visitor.paint.StyledMapRenderer
import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
import org.openstreetmap.josm.data.preferences.JosmUrls
import org.openstreetmap.josm.data.projection.Projections
import org.openstreetmap.josm.gui.NavigatableComponent
import org.openstreetmap.josm.gui.mappaint.Environment
import org.openstreetmap.josm.gui.mappaint.MultiCascade
import org.openstreetmap.josm.gui.mappaint.MapPaintStyles.IconReference
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource
import org.openstreetmap.josm.gui.mappaint.mapcss.ConditionFactory.SimpleKeyValueCondition
import org.openstreetmap.josm.gui.mappaint.mapcss.Selector.GeneralSelector
import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser
import org.openstreetmap.josm.gui.mappaint.styleelement.AreaElement
import org.openstreetmap.josm.gui.mappaint.styleelement.LineElement
import org.openstreetmap.josm.gui.mappaint.styleelement.StyleElement
import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference
import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset
import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader
import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType
import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem
import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem.MatchType
import org.openstreetmap.josm.io.CachedFile
import org.openstreetmap.josm.spi.preferences.Config
import org.openstreetmap.josm.tools.Logging
import org.openstreetmap.josm.tools.RightAndLefthandTraffic
import org.openstreetmap.josm.tools.Territories
import org.openstreetmap.josm.tools.Utils

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings

class TagInfoExtract {

    static def options
    static String image_dir
    int josm_svn_revision
    String input_file
    MapCSSStyleSource style_source
    FileWriter output_file
    String base_dir = "."
    Set tags = []

    private def cached_svnrev

    /**
     * Check if a certain tag is supported by the style as node / way / area.
     */
    abstract class Checker {

        def tag
        OsmPrimitive osm

        Checker(tag) {
            this.tag = tag
        }

        Environment apply_stylesheet(OsmPrimitive osm) {
            osm.put(tag[0], tag[1])
            MultiCascade mc = new MultiCascade()

            Environment env = new Environment(osm, mc, null, style_source)
            for (def r in style_source.rules) {
                env.clearSelectorMatchingInformation()
                if (r.selector.matches(env)) {
                    // ignore selector range
                    if (env.layer == null) {
                        env.layer = "default"
                    }
                    r.execute(env)
                }
            }
            env.layer = "default"
            return env
        }

        /**
         * Create image file from StyleElement.
         * @return the URL
         */
        def create_image(StyleElement elem_style, type, nc) {
            def img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB)
            def g = img.createGraphics()
            g.setClip(0, 0, 16, 16)
            def renderer = new StyledMapRenderer(g, nc, false)
            renderer.getSettings(false)
            elem_style.paintPrimitive(osm, MapPaintSettings.INSTANCE, renderer, false, false, false)
            def base_url = options.imgurlprefix ? options.imgurlprefix : image_dir
            def image_name = "${type}_${tag[0]}=${tag[1]}.png"
            ImageIO.write(img, "png", new File("${image_dir}/${image_name}"))
            return "${base_url}/${image_name}"
        }

        /**
         * Checks, if tag is supported and find URL for image icon in this case.
         * @param generate_image if true, create or find a suitable image icon and return URL,
         * if false, just check if tag is supported and return true or false
         */
        abstract def find_url(boolean generate_image)
    }

    @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    class NodeChecker extends Checker {
        NodeChecker(tag) {
            super(tag)
        }

        @Override
        def find_url(boolean generate_image) {
            osm = new Node(LatLon.ZERO)
            def env = apply_stylesheet(osm)
            def c = env.mc.getCascade("default")
            def image = c.get("icon-image")
            if (image) {
                if (image instanceof IconReference && !image.isDeprecatedIcon()) {
                    return find_image_url(image.iconName)
                }
            }
        }
    }

    @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    class WayChecker extends Checker {
        WayChecker(tag) {
            super(tag)
        }

        @Override
        def find_url(boolean generate_image) {
            osm = new Way()
            def nc = new NavigatableComponent()
            def n1 = new Node(nc.getLatLon(2,8))
            def n2 = new Node(nc.getLatLon(14,8))
            ((Way)osm).addNode(n1)
            ((Way)osm).addNode(n2)
            def env = apply_stylesheet(osm)
            def les = LineElement.createLine(env)
            if (les != null) {
                if (!generate_image) return true
                return create_image(les, 'way', nc)
            }
        }
    }

    @SuppressFBWarnings(value = "MF_CLASS_MASKS_FIELD")
    class AreaChecker extends Checker {
        AreaChecker(tag) {
            super(tag)
        }

        @Override
        def find_url(boolean generate_image) {
            osm = new Way()
            def nc = new NavigatableComponent()
            def n1 = new Node(nc.getLatLon(2,2))
            def n2 = new Node(nc.getLatLon(14,2))
            def n3 = new Node(nc.getLatLon(14,14))
            def n4 = new Node(nc.getLatLon(2,14))
            ((Way)osm).addNode(n1)
            ((Way)osm).addNode(n2)
            ((Way)osm).addNode(n3)
            ((Way)osm).addNode(n4)
            ((Way)osm).addNode(n1)
            def env = apply_stylesheet(osm)
            def aes = AreaElement.create(env)
            if (aes != null) {
                if (!generate_image) return true
                return create_image(aes, 'area', nc)
            }
        }
    }

    /**
     * Main method.
     */
    static main(def args) {
        parse_command_line_arguments(args)
        def script = new TagInfoExtract()
        if (!options.t || options.t == 'mappaint') {
            script.run()
        } else if (options.t == 'presets') {
            script.run_presets()
        } else if (options.t == 'external_presets') {
            script.run_external_presets()
        } else {
            System.err.println 'Invalid type ' + options.t
            if (!options.noexit) {
                System.exit(1)
            }
        }

        if (!options.noexit) {
            System.exit(0)
        }
    }

    /**
     * Parse command line arguments.
     */
    static void parse_command_line_arguments(args) {
        def cli = new CliBuilder(usage:'taginfoextract.groovy [options] [inputfile]',
            header:"Options:",
            footer:"[inputfile]           the file to process (optional, default is 'resource://styles/standard/elemstyles.mapcss')")
        cli.o(args:1, argName: "file", "output file (json), - prints to stdout (default: -)")
        cli.t(args:1, argName: "type", "the project type to be generated")
        cli._(longOpt:'svnrev', args:1, argName:"revision", "corresponding revision of the repository https://svn.openstreetmap.org/ (optional, current revision is read from the local checkout or from the web if not given, see --svnweb)")
        cli._(longOpt:'imgdir', args:1, argName:"directory", "directory to put the generated images in (default: ./taginfo-img)")
        cli._(longOpt:'noexit', "don't call System.exit(), for use from Ant script")
        cli._(longOpt:'svnweb', 'fetch revision of the repository https://svn.openstreetmap.org/ from web and not from the local repository')
        cli._(longOpt:'imgurlprefix', args:1, argName:'prefix', 'image URLs prefix for generated image files')
        cli.h(longOpt:'help', "show this help")
        options = cli.parse(args)

        if (options.h) {
            cli.usage()
            System.exit(0)
        }
        if (options.arguments().size() > 1) {
            System.err.println "Error: More than one input file given!"
            cli.usage()
            System.exit(-1)
        }
        if (options.svnrev) {
            assert Integer.parseInt(options.svnrev) > 0
        }
        image_dir = 'taginfo-img'
        if (options.imgdir) {
            image_dir = options.imgdir
        }
        def image_dir_file = new File(image_dir)
        if (!image_dir_file.exists()) {
            image_dir_file.mkdirs()
        }
    }

    void run_presets() {
        init()
        def presets = TaggingPresetReader.readAll(input_file, true)
        def tags = convert_presets(presets, "", true)
        write_json("JOSM main presets", "Tags supported by the default presets in the OSM editor JOSM", tags)
    }

    def convert_presets(Iterable<TaggingPreset> presets, String descriptionPrefix, boolean addImages) {
        def tags = []
        for (TaggingPreset preset : presets) {
            for (KeyedItem item : Utils.filteredCollection(preset.data, KeyedItem.class)) {
                def values
                switch (MatchType.ofString(item.match)) {
                    case MatchType.KEY_REQUIRED: values = item.getValues(); break;
                    case MatchType.KEY_VALUE_REQUIRED: values = item.getValues(); break;
                    default: values = [];
                }
                for (String value : values) {
                    def tag = [
                            description: descriptionPrefix + preset.name,
                            key: item.key,
                            value: value,
                    ]
                    def otypes = preset.types.collect {
                        it == TaggingPresetType.CLOSEDWAY ? "area" :
                            (it == TaggingPresetType.MULTIPOLYGON ? "relation" : it.toString().toLowerCase(Locale.ENGLISH))
                    }
                    if (!otypes.isEmpty()) tag += [object_types: otypes]
                    if (addImages && preset.iconName) tag += [icon_url: find_image_url(preset.iconName)]
                    tags += tag
                }
            }
        }
        return tags
    }

    void run_external_presets() {
        init()
        TaggingPresetReader.setLoadIcons(false)
        def sources = new TaggingPresetPreference.TaggingPresetSourceEditor().loadAndGetAvailableSources()
        def tags = []
        for (def source : sources) {
            if (source.url.startsWith("resource")) {
                // default presets
                continue;
            }
            try {
                println "Loading ${source.url}"
                def presets = TaggingPresetReader.readAll(source.url, false)
                def t = convert_presets(presets, source.title + " ", false)
                println "Converting ${t.size()} presets of ${source.title}"
                tags += t
            } catch (Exception ex) {
                System.err.println("Skipping ${source.url} due to error")
                ex.printStackTrace()
            }
        }
        write_json("JOSM user presets", "Tags supported by the user contributed presets in the OSM editor JOSM", tags)
    }

    void run() {
        init()
        parse_style_sheet()
        collect_tags()

        def tags = tags.collect {
            def tag = it
            def types = []
            def final_url = null

            def node_url = new NodeChecker(tag).find_url(true)
            if (node_url) {
                types += 'node'
                final_url = node_url
            }
            def way_url = new WayChecker(tag).find_url(final_url == null)
            if (way_url) {
                types += 'way'
                if (!final_url) {
                    final_url = way_url
                }
            }
            def area_url = new AreaChecker(tag).find_url(final_url == null)
            if (area_url) {
                types += 'area'
                if (!final_url) {
                    final_url = area_url
                }
            }

            def obj = [key: tag[0], value: tag[1]]
            if (types) obj += [object_types: types]
            if (final_url) obj += [icon_url: final_url]
            obj
        }

        write_json("JOSM main mappaint style", "Tags supported by the main mappaint style in the OSM editor JOSM", tags)
    }

    void write_json(String name, String description, List<Map<String, ?>> tags) {
        def config = [:]
        config[JsonGenerator.PRETTY_PRINTING] = output_file == null
        def writer = output_file != null ? output_file : new StringWriter()
        def json = Json.createWriterFactory(config).createWriter(writer)
        try {
            def project = Json.createObjectBuilder()
                .add("name", name)
                .add("description", description)
                .add("project_url", "https://josm.openstreetmap.de/")
                .add("icon_url", "https://josm.openstreetmap.de/export/7770/josm/trunk/images/logo_16x16x8.png")
                .add("contact_name", "JOSM developer team")
                .add("contact_email", "josm-dev@openstreetmap.org")
            def jsonTags = Json.createArrayBuilder()
            for (def t : tags) {
                def o = Json.createObjectBuilder()
                for (def e : t.entrySet()) {
                    def val = e.getValue()
                    if (e.getValue() instanceof List) {
                        def arr = Json.createArrayBuilder()
                        for (def v : e.getValue()) {
                            arr.add(v)
                        }
                        val = arr.build()
                    }
                    o.add(e.getKey(), val)
                }
                jsonTags.add(o.build())
            }
            json.writeObject(Json.createObjectBuilder()
                .add("data_format", 1)
                .add("data_updated", DateTimeFormatter.ofPattern("yyyyMMdd'T'hhmmss'Z'").withZone(ZoneId.of("Z")).format(Instant.now()))
                .add("project", project.build())
                .add("tags", jsonTags.build())
                .build())
        } finally {
            json.close()
        }

        if (output_file != null) {
            output_file.close()
        } else {
            print writer.toString()
        }
    }

    /**
     * Initialize the script.
     */
    def init() {
        Main.determinePlatformHook()
        Logging.setLogLevel(Logging.LEVEL_INFO)
        Main.pref.enableSaveOnPut(false)
        Config.setPreferencesInstance(Main.pref)
        Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance())
        Config.setUrlsProvider(JosmUrls.getInstance())
        Main.setProjection(Projections.getProjectionByCode("EPSG:3857"))
        Path tmpdir = Files.createTempDirectory(FileSystems.getDefault().getPath(base_dir), "pref")
        tmpdir.toFile().deleteOnExit()
        System.setProperty("josm.home", tmpdir.toString())
        DeleteCommand.setDeletionCallback(DeleteAction.defaultDeletionCallback)
        Territories.initialize()
        RightAndLefthandTraffic.initialize()

        josm_svn_revision = Version.getInstance().getVersion()
        assert josm_svn_revision != Version.JOSM_UNKNOWN_VERSION

        if (options.arguments().size() == 0 && (!options.t || options.t == 'mappaint')) {
            input_file = "resource://styles/standard/elemstyles.mapcss"
        } else if (options.arguments().size() == 0 && options.t == 'presets') {
            input_file = "resource://data/defaultpresets.xml"
        } else {
            input_file = options.arguments()[0]
        }

        output_file = null
        if (options.o && options.o != "-") {
            output_file = new FileWriter(options.o)
        }
    }

    /**
     * Determine full image url (can refer to JOSM or OSM repository).
     */
    def find_image_url(String path) {
        def f = new File("${base_dir}/images/styles/standard/${path}")
        if (f.exists()) {
            def rev = osm_svn_revision()
            return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
        }
        f = new File("${base_dir}/images/${path}")
        if (f.exists()) {
            if (path.startsWith("images/styles/standard/")) {
                path = path.substring("images/styles/standard/".length())
                def rev = osm_svn_revision()
                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
            } else if (path.startsWith("styles/standard/")) {
                path = path.substring("styles/standard/".length())
                def rev = osm_svn_revision()
                return "https://trac.openstreetmap.org/export/${rev}/subversion/applications/share/map-icons/classic.small/${path}"
            } else {
                return "https://josm.openstreetmap.de/export/${josm_svn_revision}/josm/trunk/images/${path}"
            }
        }
        assert false, "Cannot find image url for ${path}"
    }

    /**
     * Get revision for the repository https://svn.openstreetmap.org.
     */
    def osm_svn_revision() {
        if (cached_svnrev != null) return cached_svnrev
        if (options.svnrev) {
            cached_svnrev = Integer.parseInt(options.svnrev)
            return cached_svnrev
        }
        def xml
        if (options.svnweb) {
            xml = "svn info --xml https://svn.openstreetmap.org/applications/share/map-icons/classic.small".execute().text
        } else {
            xml = "svn info --xml ${base_dir}/images/styles/standard/".execute().text
        }

        def svninfo = new XmlParser().parseText(xml)
        def rev = svninfo.entry.'@revision'[0]
        cached_svnrev = Integer.parseInt(rev)
        assert cached_svnrev > 0
        return cached_svnrev
    }

    /**
     * Read the style sheet file and parse the MapCSS code.
     */
    def parse_style_sheet() {
        def file = new CachedFile(input_file)
        def stream = file.getInputStream()
        def parser = new MapCSSParser(stream, "UTF-8", MapCSSParser.LexicalState.DEFAULT)
        style_source = new MapCSSStyleSource("")
        style_source.url = ""
        parser.sheet(style_source)
    }

    /**
     * Collect all the tag from the style sheet.
     */
    def collect_tags() {
        for (rule in style_source.rules) {
            def selector = rule.selector
            if (selector instanceof GeneralSelector) {
                def conditions = selector.getConditions()
                for (cond in conditions) {
                    if (cond instanceof SimpleKeyValueCondition) {
                        tags.add([cond.k, cond.v])
                    }
                }
            }
        }
    }
}
