import importlib.util
import os
from dataclasses import dataclass
from typing import Optional

import click
from jinja2 import Environment, FileSystemLoader, Template

from linkml._version import __version__
from linkml.generators.oocodegen import OOCodeGenerator
from linkml.utils.generator import shared_arguments
from linkml_runtime.linkml_model.meta import TypeDefinition

default_template = """
{#-

  Jinja2 Template for a Java class with Lombok @Data annotation
  Annotation details at https://projectlombok.org
-#}
package {{ doc.package }};

{% if metamodel_version -%}
/* metamodel_version: {{metamodel_version}} */
{% endif -%}
{% if model_version -%}
/* version: {{model_version}} */
{% endif -%}

{% if cls -%}
import java.util.List;
import lombok.*;

{%   if cls.source_class.description -%}
/**
  {{ cls.source_class.description }}
**/
{%   endif -%}
@Data
@EqualsAndHashCode(callSuper=false)
public{% if cls.abstract %} abstract{% endif %} class {{ cls.name }} {% if cls.is_a -%} extends {{ cls.is_a }} {%- endif %} {
{%   for f in cls.fields %}
  private {{f.range}} {{ f.name }};
{%-  endfor %}

{% elif enum -%}

import java.util.Map;
import java.util.HashMap;
import java.util.Collections;

import com.fasterxml.jackson.annotation.JsonCreator;

{% if enum.description %}/**
 * {{ enum.description }}
 */
{% endif -%}
public enum {{ enum.name }} {
{%- for value in enum.values[0:-1] %}
{% if value.description %}
  /**
   * {{ value.description }}
   */
{%- endif %}
  {{ value.label|upper }}("{{ value.text }}"),
{%- endfor %}
{%- if enum.values %}
{% if enum.values[-1].description %}
  /**
   * {{ enum.values[-1].description }}
   */
{%- endif %}
  {{ enum.values[-1].label|upper }}("{{ enum.values[-1].text }}");
  {%- else %}
  ;{% endif %}

  private final static Map<String, {{ enum.name }}> MAP;

  static {
    Map<String, {{ enum.name }}> map = new HashMap<String, {{ enum.name }}>();
    for ( {{ enum.name }} value : {{ enum.name }}.values() ) {
      map.put(value.toString(), value);
    }

    MAP = Collections.unmodifiableMap(map);
  }

  private final String repr;

  {{ enum.name }}(String repr) {
    this.repr = repr;
  }

  @Override
  public String toString() {
    return repr;
  }

  @JsonCreator
  public static {{ enum.name }} fromString(String v) {
    return MAP.get(v);
  }

{% endif -%}
}"""  # noqa: E501

TYPEMAP = {
    "xsd:string": "String",
    "xsd:integer": "Integer",
    "xsd:float": "Float",
    "xsd:double": "Double",
    "xsd:boolean": "boolean",
    "xsd:dateTime": "ZonedDateTime",
    "xsd:date": "LocalDate",
    "xsd:time": "Instant",
    "xsd:anyURI": "String",
    "xsd:decimal": "BigDecimal",
}

TYPE_DEFAULTS = {"boolean": "false", "int": "0", "float": "0f", "double": "0d", "String": '""'}


@dataclass
class JavaGenerator(OOCodeGenerator):
    """
    Generates java code from a LinkML schema.

    Two styles are supported:

    - java classes, using lombok annotations
    - java records
    """

    # ClassVars
    generatorname = os.path.basename(__file__)
    generatorversion = "0.0.1"
    valid_formats = ["java"]
    file_extension = "java"

    # ObjectVars
    generate_records: bool = False
    """If True then use java records (introduced in java 14) rather than classes"""

    template_file: Optional[str] = None

    gen_classvars: bool = True
    gen_slots: bool = True
    genmeta: bool = False
    emit_metadata: bool = True

    def default_value_for_type(self, typ: str) -> str:
        return TYPE_DEFAULTS.get(typ, "null")

    def map_type(self, t: TypeDefinition, required: bool = False) -> str:
        if t.uri:
            # only return a Integer, Double Float when required == false
            typ = TYPEMAP.get(t.uri)
            if required and (typ == "Double" or typ == "Float"):
                typ = typ.lower()
            elif required and typ == "Integer":
                typ = "int"
            return typ
        elif t.typeof:
            return self.map_type(self.schemaview.get_type(t.typeof))
        else:
            raise ValueError(f"{t} cannot be mapped to a type")

    def serialize(self, directory: str, **kwargs) -> None:
        if self.generate_records:
            package_dir = os.path.dirname(importlib.util.find_spec(__name__).origin)
            javagen_folder = os.path.join(package_dir, "javagen", "")
            loader = FileSystemLoader(javagen_folder)
            env = Environment(loader=loader)
            template_obj = env.get_template("java_record_template.jinja2")
        elif self.template_file is not None:
            with open(self.template_file) as template_file:
                template_obj = Template(template_file.read())
        else:
            template_obj = Template(default_template)

        oodocs = self.create_documents()
        self.directory = directory
        for oodoc in oodocs:
            if oodoc.classes:
                cls = oodoc.classes[0]
                enum = None
            else:
                cls = None
                enum = oodoc.enums[0]
            code = template_obj.render(
                doc=oodoc,
                cls=cls,
                enum=enum,
                metamodel_version=self.schema.metamodel_version,
                model_version=self.schema.version,
            )

            os.makedirs(directory, exist_ok=True)
            filename = f"{oodoc.name}.java"
            path = os.path.join(directory, filename)
            with open(path, "w", encoding="UTF-8") as stream:
                stream.write(code)


@shared_arguments(JavaGenerator)
@click.option(
    "--output-directory",
    default="output",
    show_default=True,
    help="Output directory for individually generated class files",
)
@click.option("--package", help="Package name where relevant for generated class files")
@click.option("--template-file", help="Optional jinja2 template to use for class generation")
@click.option(
    "--generate-records/--no-generate-records",
    default=False,
    help="Optional Java 17 record implementation",
)
@click.option("--true-enums/--no-true-enums", default=False, help="Treat enums as distinct types rather than strings")
@click.version_option(__version__, "-V", "--version")
@click.command(name="java")
def cli(
    yamlfile,
    output_directory=None,
    package=None,
    template_file=None,
    generate_records=False,
    head=True,
    emit_metadata=False,
    genmeta=False,
    classvars=True,
    slots=True,
    true_enums=False,
    **args,
):
    """Generate java classes to represent a LinkML model"""
    JavaGenerator(
        yamlfile,
        package=package,
        template_file=template_file,
        generate_records=generate_records,
        emit_metadata=head,
        genmeta=genmeta,
        gen_classvars=classvars,
        gen_slots=slots,
        true_enums=true_enums,
        **args,
    ).serialize(output_directory, **args)


if __name__ == "__main__":
    cli()
