import os
import re
from dataclasses import dataclass
from typing import Any, Callable, Optional, Union

import click
from jsonasobj2 import JsonObj, values
from linkml_runtime.linkml_model.meta import (
    ClassDefinition,
    ClassDefinitionName,
    Element,
    EnumDefinition,
    SlotDefinition,
    SubsetDefinition,
    TypeDefinition,
)
from linkml_runtime.utils.formatutils import be, camelcase, underscore

from linkml._version import __version__
from linkml.generators.yumlgen import YumlGenerator
from linkml.utils.deprecation import deprecation_warning
from linkml.utils.generator import Generator, shared_arguments
from linkml.utils.typereferences import References


@dataclass
class MarkdownGenerator(Generator):
    """
    Generates markdown documentation for a LinkML schema

    Each schema element (class, slot, type, enum) is translated into its own markdown file;
    additionally, an index.md is generated that links everything together.

    The markdown is suitable for deployment as a MkDocs or Sphinx site

    .. admonition:: Deprecated
        :class: warning

            The MarkdownGenerator class is being deprecated in favor of DocGenerator which can
            be found at the following path – `linkml/generators/docgen.py`. Going forward, DocGenerator
            which can be invoked using the `gen-doc` command will be the preferred way to generate
            Markdown documentation files for LinkML schemas.

            .. deprecated:: v1.9.1

            Recommendation: Update to use `gen-doc`
    """

    def __post_init__(self) -> None:
        deprecation_warning("gen-markdown")
        super().__post_init__()

    # ClassVars
    generatorname = os.path.basename(__file__)
    generatorversion = "0.2.1"
    directory_output = True
    valid_formats = ["md"]
    visit_all_class_slots = False
    uses_schemaloader = True

    # ObjectVars
    directory: Optional[str] = None
    image_directory: Optional[str] = None
    classes: set[ClassDefinitionName] = None
    image_dir: bool = False
    index_file: str = "index.md"
    noimages: bool = False
    noyuml: bool = False
    no_types_dir: bool = False
    warn_on_exist: bool = False
    gen_classes: Optional[set[ClassDefinitionName]] = None
    gen_classes_neighborhood: Optional[References] = None

    def visit_schema(
        self,
        directory: str = None,
        classes: set[ClassDefinitionName] = None,
        image_dir: bool = False,
        index_file: str = "index.md",
        noimages: bool = False,
        **_,
    ) -> str:
        self.gen_classes = classes if classes else []
        for cls in self.gen_classes:
            if cls not in self.schema.classes:
                raise ValueError("Unknown class name: {cls}")
        if self.gen_classes:
            self.gen_classes_neighborhood = self.neighborhood(list(self.gen_classes))

        self.directory = directory
        if directory:
            os.makedirs(directory, exist_ok=True)
        elif image_dir:
            raise ValueError("Image directory can only be used with '-d' option")
        if image_dir:
            self.image_directory = os.path.join(directory, "images")
            if not noimages:
                os.makedirs(self.image_directory, exist_ok=True)
        self.noimages = noimages
        if not self.no_types_dir:
            os.makedirs(os.path.join(directory, "types"), exist_ok=True)

        with open(self.exist_warning(directory, index_file), "w", encoding="UTF-8") as ixfile:
            items = []
            items.append(self.frontmatter(f"{self.schema.name}"))
            items.append(
                self.para(
                    f"**metamodel version:** {self.schema.metamodel_version}\n\n**version:** {self.schema.version}"
                )
            )
            items.append(self.para(be(self.schema.description)))

            items.append(self.header(3, "Classes"))
            for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
                if not cls.is_a and not cls.mixin and self.is_secondary_ref(cls.name):
                    items.append(self.class_hier(cls))

            items.append(self.header(3, "Mixins"))
            for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
                if cls.mixin and self.is_secondary_ref(cls.name):
                    items.append(self.class_hier(cls))

            items.append(self.header(3, "Slots"))
            for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
                if not slot.is_a and self.is_secondary_ref(slot.name):
                    items.append(self.pred_hier(slot))

            items.append(self.header(3, "Enums"))
            for enu in sorted(self.schema.enums.values(), key=lambda e: e.name):
                items.append(self.enum_hier(enu))

            items.append(self.header(3, "Subsets"))
            for subset in sorted(self.schema.subsets.values(), key=lambda s: s.name):
                items.append(self.bullet(self.subset_link(subset, use_desc=True), 0))

            items.append(self.header(3, "Types"))
            items.append(self.header(4, "Built in"))
            for builtin_name in sorted(self.synopsis.typebases.keys()):
                items.append(self.bullet(f"**{builtin_name}**"))
            items.append(self.header(4, "Defined"))
            for typ in sorted(self.schema.types.values(), key=lambda t: t.name):
                if self.is_secondary_ref(typ.name):
                    if typ.typeof:
                        typ_typ = self.type_link(typ.typeof)
                    else:
                        typ_typ = f"**{typ.base}**"

                    items.append(self.bullet(self.type_link(typ, after_link=f" ({typ_typ})", use_desc=True)))
            items = [i for i in items if i is not None]
            out = "\n".join(items) + "\n"
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            ixfile.write(out)
        return out

    def visit_class(self, cls: ClassDefinition) -> str:
        # allow client to relabel metamodel
        mixin_local_name = self.get_metamodel_slot_name("Mixin")
        class_local_name = self.get_metamodel_slot_name("Class")

        if self.gen_classes and cls.name not in self.gen_classes:
            return ""

        with open(self.exist_warning(self.dir_path(cls)), "w", encoding="UTF-8") as clsfile:
            items = []
            class_curi = self.namespaces.uri_or_curie_for(str(self.namespaces._base), camelcase(cls.name))
            class_uri = self.namespaces.uri_for(class_curi)
            items.append(self.element_header(cls, cls.name, class_curi, class_uri))
            items.append("")
            if not self.noyuml:
                if self.image_directory:
                    yg = YumlGenerator(self)
                    yg.serialize(
                        classes=[cls.name],
                        directory=self.image_directory,
                        load_image=not self.noimages,
                    )
                    img_url = os.path.join("images", os.path.basename(yg.output_file_name))
                else:
                    yg = YumlGenerator(self)
                    img_url = (
                        yg.serialize(classes=[cls.name]).replace("?", "%3F").replace(" ", "%20").replace("|", "&#124;")
                    ).rstrip()

                items.append(f"[![img]({img_url})]({img_url})")

            if cls.id_prefixes:
                items.append(self.header(2, "Identifier prefixes"))
                for p in cls.id_prefixes:
                    items.append(self.bullet(f"{p}"))

            if cls.is_a is not None:
                items.append(self.header(2, "Parents"))
                items.append(self.bullet(f" is_a: {self.class_link(cls.is_a, use_desc=True)}"))
            if cls.mixins:
                items.append(self.header(2, f"Uses {mixin_local_name}"))
                for mixin in cls.mixins:
                    items.append(self.bullet(f" mixin: {self.class_link(mixin, use_desc=True)}"))

            if cls.name in self.synopsis.isarefs:
                items.append(self.header(2, "Children"))
                for child in sorted(self.synopsis.isarefs[cls.name].classrefs):
                    items.append(self.bullet(f"{self.class_link(child, use_desc=True)}"))

            if cls.name in self.synopsis.mixinrefs:
                items.append(self.header(2, f"{mixin_local_name} for"))
                for mixin in sorted(self.synopsis.mixinrefs[cls.name].classrefs):
                    items.append(self.bullet(f"{self.class_link(mixin, use_desc=True, after_link='(mixin)')}"))

            if cls.name in self.synopsis.classrefs:
                items.append(self.header(2, f"Referenced by {class_local_name}"))
                for sn in sorted(self.synopsis.classrefs[cls.name].slotrefs):
                    slot = self.schema.slots[sn]
                    if slot.range == cls.name:
                        items.append(
                            self.bullet(
                                f" **{self.class_link(slot.domain)}** "
                                f"*{self.slot_link(slot, add_subset=False)}*{self.predicate_cardinality(slot)}  "
                                f"**{self.class_type_link(slot.range)}**"
                            )
                        )

            items.append(self.header(2, "Attributes"))

            # List all of the slots that directly belong to the class
            slot_list = [slot for slot in [self.schema.slots[sn] for sn in cls.slots]]
            own_slots = [slot for slot in slot_list if cls.name in slot.domain_of]
            if own_slots:
                items.append(self.header(3, "Own"))
                for slot in own_slots:
                    items.append(self.slot_field(cls, slot))
                    slot_list.remove(slot)

            # List all of the inherited slots
            ancestors = set(self.ancestors(cls))
            inherited_slots = [slot for slot in slot_list if set(slot.domain_of).intersection(ancestors)]
            if inherited_slots:
                items.append(self.header(3, "Inherited from " + cls.is_a + ":"))
                for inherited_slot in inherited_slots:
                    items.append(self.slot_field(cls, inherited_slot))
                    slot_list.remove(inherited_slot)

            # List all of the slots acquired through mixing
            mixed_in_classes = set()
            for mixin in cls.mixins:
                mixed_in_classes.add(mixin)
                mixed_in_classes.update(set(self.ancestors(self.schema.classes[mixin])))
            for slot in slot_list:
                mixers = set(slot.domain_of).intersection(mixed_in_classes)
                for mixer in mixers:
                    items.append(self.header(3, "Mixed in from " + mixer + ":"))
                    items.append(self.slot_field(cls, slot))

            items.append(self.element_properties(cls))
            out = "\n".join(items)
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            clsfile.write(out)
        return out

    def visit_type(self, typ: TypeDefinition) -> str:
        with open(self.exist_warning(self.dir_path(typ)), "w", encoding="UTF-8") as typefile:
            type_uri = typ.definition_uri
            type_curie = self.namespaces.curie_for(type_uri)
            out = self.element_header(typ, typ.name, type_curie, type_uri)

            out = "\n".join([out, "|  |  |  |"])
            out = "\n".join([out, "| --- | --- | --- |"])
            if typ.typeof:
                out = "\n".join([out, f"| Parent type | | {self.class_type_link(typ.typeof)} |"])
            out = "\n".join([out, f"| Root (builtin) type | | **{typ.base}** |"])
            if typ.repr:
                out = "\n".join([out, f"| Representation | | {typ.repr} |"])
            out += self.element_properties(typ)
            out += "\n"
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            typefile.write(out)
        return out

    def visit_slot(self, aliased_slot_name: str, slot: SlotDefinition) -> str:
        with open(self.exist_warning(self.dir_path(slot)), "w", encoding="UTF-8") as slotfile:
            items = []
            slot_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(slot.name))
            slot_uri = self.namespaces.uri_for(slot_curie)
            items.append(self.element_header(slot, aliased_slot_name, slot_curie, slot_uri))

            items.append(self.header(2, "Domain and Range"))
            items.append(
                f"{self.class_link(slot.domain)} &#8594;{self.predicate_cardinality(slot)} "
                f"{self.class_type_link(slot.range)}"
            )

            items.append(self.header(2, "Parents"))
            if slot.is_a:
                items.append(self.bullet(f" is_a: {self.slot_link(slot.is_a)}"))

            items.append(self.header(2, "Children"))
            if slot.name in sorted(self.synopsis.isarefs):
                for child in sorted(self.synopsis.isarefs[slot.name].slotrefs):
                    items.append(self.bullet(f" {self.slot_link(child)}"))

            items.append(self.header(2, "Used by"))
            if slot.name in sorted(self.synopsis.slotrefs):
                for rc in sorted(self.synopsis.slotrefs[slot.name].classrefs):
                    items.append(self.bullet(f"{self.class_link(rc)}"))
            if aliased_slot_name == "relation":
                if slot.subproperty_of:
                    reifies = (
                        self.slot_link(slot.subproperty_of)
                        if slot.subproperty_of in self.schema.slots
                        else slot.subproperty_of
                    )
                    items.append(self.bullet(f" reifies: {reifies}"))
            items.append(self.element_properties(slot))
            out = "\n".join(items)
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            slotfile.write(out)
        return out

    def visit_enum(self, enum: EnumDefinition) -> str:
        with open(self.exist_warning(self.dir_path(enum)), "w", encoding="UTF-8") as enumfile:
            items = []
            enum_curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(enum.name))
            enum_uri = self.namespaces.uri_for(enum_curie)
            items.append(self.element_header(obj=enum, name=enum.name, curie=enum_curie, uri=enum_uri))
            items.append(self.element_properties(enum))
            out = "\n".join(items)
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            enumfile.write(out)
        return out

    def visit_subset(self, subset: SubsetDefinition) -> str:
        with open(self.exist_warning(self.dir_path(subset)), "w", encoding="UTF-8") as subsetfile:
            items = []
            curie = self.namespaces.uri_or_curie_for(str(self.namespaces._base), underscore(subset.name))
            uri = self.namespaces.uri_for(curie)
            items.append(self.element_header(obj=subset, name=subset.name, curie=curie, uri=uri))
            # TODO: consider showing hierarchy within a subset
            items.append(self.header(3, "Classes"))
            for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
                if not cls.mixin:
                    if cls.in_subset and subset.name in cls.in_subset:
                        items.append(self.bullet(self.class_link(cls, use_desc=True), 0))
            items.append(self.header(3, "Mixins"))
            for cls in sorted(self.schema.classes.values(), key=lambda c: c.name):
                if cls.mixin:
                    if cls.in_subset and subset.name in cls.in_subset:
                        items.append(self.bullet(self.class_link(cls, use_desc=True), 0))
            items.append(self.header(3, "Slots"))
            for slot in sorted(self.schema.slots.values(), key=lambda s: s.name):
                if slot.in_subset and subset.name in slot.in_subset:
                    items.append(self.bullet(self.slot_link(slot, use_desc=True), 0))
            items.append(self.header(3, "Types"))
            for type in sorted(self.schema.types.values(), key=lambda s: s.name):
                if type.in_subset and subset.name in type.in_subset:
                    items.append(self.bullet(self.type_link(type, use_desc=True), 0))
            items.append(self.header(3, "Enums"))
            for enum in sorted(self.schema.enums.values(), key=lambda s: s.name):
                if enum.in_subset and subset.name in enum.in_subset:
                    items.append(self.bullet(self.enum_link(enum, use_desc=True), 0))
            items.append(self.element_properties(subset))
            out = "\n".join(items)
            out = pad_heading(out)
            out = out.rstrip() + "\n"
            subsetfile.write(out)
        return out

    def element_header(self, obj: Element, name: str, curie: str, uri: str) -> str:
        if isinstance(obj, TypeDefinition):
            obj_type = "Type"
        elif isinstance(obj, ClassDefinition):
            obj_type = "Class"
        elif isinstance(obj, SlotDefinition):
            obj_type = "Slot"
        elif isinstance(obj, EnumDefinition):
            obj_type = "Enum"
        elif isinstance(obj, SubsetDefinition):
            obj_type = "Subset"
        else:
            obj_type = "Class"

        header_label = f"{obj_type}: ~~{name}~~ _(deprecated)_" if obj.deprecated else f"{obj_type}: {name}"
        out = self.header(1, header_label)

        out += self.para(be(obj.description))
        out = "\n".join([out, f"URI: [{curie}]({uri})", ""])
        return out

    def element_properties(self, obj: Element) -> str:
        def identity(e: Any) -> Any:
            return e

        def prop_list(
            title: str,
            entries: Union[list, dict],
            formatter: Optional[Callable[[Element], str]] = None,
        ) -> Optional[str]:
            if formatter is None:
                formatter = identity
            if isinstance(entries, (dict, JsonObj)):
                entries = list(values(entries))
            if entries:
                items = []
                items.append(f"| **{title}:** | | {formatter(entries[0])} |")
                for entry in entries[1:]:
                    items.append(f"|  | | {formatter(entry)} |")
                return "\n".join(items)

        def enum_list(title: str, obj: EnumDefinition) -> str:
            # This data is from the enum provided in the YAML
            items = []
            items.append(self.header(2, title))
            items.append("| Text | Description | Meaning | Other Information |")
            items.append("| :--- | :---: | :---: | ---: |")

            for item, item_info in obj.permissible_values.items():
                text = ""
                desc = ""
                meaning = ""
                other = {}
                for k in item_info:
                    if item_info[k] is not None and len(item_info[k]) > 0:
                        if k == "text":
                            text = item_info[k]
                        elif k == "description":
                            desc = item_info[k]
                        elif k == "meaning":
                            meaning = item_info[k]
                        else:
                            other[k] = item_info[k]
                if not other:
                    other = ""
                items.append(f"| {text} | {desc} | {meaning} | {other} |")
            return "\n".join(items)

        items = []

        items.append(prop_list("Aliases", obj.aliases))
        items.append(
            prop_list(
                "Local names",
                obj.local_names,
                lambda e: f"{e.local_name_value} ({e.local_name_source})",
            )
        )
        items.append(prop_list("Mappings", obj.mappings))
        items.append(
            prop_list(
                "Alt Descriptions",
                obj.alt_descriptions,
                lambda e: f"{e.description} ({e.source})",
            )
        )
        # todos
        # notes
        items.append(prop_list("Comments", obj.comments))
        items.append(prop_list("Examples", obj.examples))
        items.append(prop_list("In Subsets", obj.in_subset))
        # from_schema
        # imported_from
        items.append(prop_list("See also", [f"[{v}]({v})" for v in obj.see_also]))
        items.append(prop_list("Exact Mappings", obj.exact_mappings))
        items.append(prop_list("Close Mappings", obj.close_mappings))
        items.append(prop_list("Narrow Mappings", obj.narrow_mappings))
        items.append(prop_list("Broad Mappings", obj.broad_mappings))
        items.append(prop_list("Related Mappings", obj.related_mappings))

        items = [i for i in items if i is not None]
        if len(items) > 0:
            header = "\n".join([self.header(2, "Other properties"), "|  |  |  |", "| --- | --- | --- |"])
            items.insert(0, header)

        #       - exact mappings
        #       - close mappings
        #       - related mappings
        #       - deprecated element has exact replacement
        #       - deprecated element has possible replacement
        if type(obj) is EnumDefinition:
            items.insert(0, enum_list("Permissible Values", obj))
            items.insert(1, "\n")

        out = "\n".join(items)
        return out

    def class_hier(self, cls: ClassDefinition, level=0) -> str:
        items = []
        items.append(self.bullet(self.class_link(cls, use_desc=True), level))
        if cls.name in sorted(self.synopsis.isarefs):
            for child in sorted(self.synopsis.isarefs[cls.name].classrefs):
                items.append(self.class_hier(self.schema.classes[child], level + 1))
        return "\n".join(items) if items else None

    def pred_hier(self, slot: SlotDefinition, level=0) -> str:
        items = []
        items.append(self.bullet(self.slot_link(slot, use_desc=True), level))
        if slot.name in sorted(self.synopsis.isarefs):
            for child in sorted(self.synopsis.isarefs[slot.name].slotrefs):
                items.append(self.pred_hier(self.schema.slots[child], level + 1))
        return "\n".join(items) if items else None

    def enum_hier(self, enum: EnumDefinition, level=0) -> str:
        items = []
        items.append(self.bullet(self.enum_link(enum, use_desc=True), level))
        if enum.name in sorted(self.synopsis.isarefs):
            for child in sorted(self.synopsis.isarefs[enum.name].classrefs):
                items.append(self.enum_hier(self.schema.enums[child], level + 1))
        return "\n".join(items) if items else None

    def dir_path(
        self,
        obj: Union[ClassDefinition, SlotDefinition, TypeDefinition, EnumDefinition],
    ) -> str:
        filename = (
            self.formatted_element_name(obj)
            if isinstance(obj, ClassDefinition)
            else (
                underscore(obj.name)
                if isinstance(obj, SlotDefinition)
                else underscore(obj.name)
                if isinstance(obj, EnumDefinition)
                else camelcase(obj.name)
            )
        )
        subdir = "/types" if isinstance(obj, TypeDefinition) and not self.no_types_dir else ""
        return f"{self.directory}{subdir}/{filename}.md"

    def is_secondary_ref(self, en: str) -> bool:
        """Determine whether 'en' is the name of something in the neighborhood of the requested classes

        @param en: element name
        @return: True if 'en' is the name of a slot, class or type in the immediate neighborhood of of what we are
        building
        """
        if not self.gen_classes:
            return True
        elif en in self.schema.classes:
            return en in self.gen_classes_neighborhood.classrefs
        elif en in self.schema.slots:
            return en in self.gen_classes_neighborhood.slotrefs
        elif en in self.schema.types:
            return en in self.gen_classes_neighborhood.typerefs
        else:
            return True

    def slot_field(self, cls: ClassDefinition, slot: SlotDefinition) -> str:
        items = []
        items.append(self.bullet(f"{self.slot_link(slot)}{self.predicate_cardinality(slot)}"))
        if slot.description:
            items.append(self.bullet(f"Description: {slot.description}", level=1))
        items.append(self.bullet(f"Range: {self.class_type_link(slot.range)}", level=1))
        # if slot.subproperty_of:
        #     self.bullet(f'edge label: {self.slot_link(slot.subproperty_of)}', level=1)
        for example in slot.examples:
            items.append(
                self.bullet(
                    f"Example: {getattr(example, 'value', ' ')} {getattr(example, 'description', ' ')}",
                    level=1,
                )
            )
        # if slot.name not in self.own_slot_names(cls):
        #     self.bullet(f'inherited from: {self.class_link(slot.domain)}', level=1)
        if slot.in_subset:
            ssl = ",".join(slot.in_subset)
            items.append(self.bullet(f"in subsets: ({ssl})", level=1))
        return "\n".join(items)

    def to_uri(self, uri_or_curie: str) -> str:
        """Return the URI for the slot if known"""
        if ":/" in uri_or_curie:
            return uri_or_curie
        elif self.namespaces.prefix_for(uri_or_curie) in self.namespaces:
            return self.namespaces.uri_for(uri_or_curie)
        else:
            return "*(Unknown namespace)*"

    # --
    # FORMATTING
    # --
    @staticmethod
    def predicate_cardinality(slot: SlotDefinition) -> str:
        """Emit cardinality for a suffix on a predicate"""
        if slot.multivalued:
            card_str = "1..\\*" if slot.required else "0..\\*"
        else:
            card_str = "1..1" if slot.required else "0..1"
        return f"  <sub>{card_str}</sub>"

    @staticmethod
    def range_cardinality(slot: SlotDefinition) -> str:
        """Emits cardinality decorator at end of type"""
        if slot.multivalued:
            card_str = "1..\\*" if slot.required else "0..\\*"
        else:
            card_str = "1..1" if slot.required else "0..1"
        return f"  <sub><b>{card_str}</b></sub>"

    @staticmethod
    def anchor(id_: str) -> str:
        return f'<a name="{id_}">'

    @staticmethod
    def anchorend() -> str:
        return "</a>"

    def header(self, level: int, txt: str) -> str:
        txt = self.get_metamodel_slot_name(txt)
        out = f"\n{'#' * level} {txt}\n"
        return out

    @staticmethod
    def para(txt: str) -> str:
        return f"\n{txt}\n"

    @staticmethod
    def bullet(txt: str, level=0) -> str:
        return f"{'    ' * level} * {txt}"

    def frontmatter(self, thingtype: str, layout="default") -> str:
        return self.header(1, thingtype)
        # print(f'---\nlayout: {layout}\n---\n')

    def bbin(self, obj: Element) -> str:
        """Boldify built in types

        @param obj: object name or id
        @return:
        """
        return obj.name if isinstance(obj, Element) else f"**{obj}**" if obj in self.synopsis.typebases else obj

    def desc_for(self, obj: Element, doing_descs: bool) -> str:
        """Return a description for object if it is unique (different than its parent)

        @param obj: object to be described
        @param doing_descs: If false, always return an empty string
        @return: text or empty string
        """
        if obj.description and doing_descs:
            if isinstance(obj, SlotDefinition) and obj.is_a:
                parent = self.schema.slots[obj.is_a]
            elif isinstance(obj, ClassDefinition) and obj.is_a:
                parent = self.schema.classes[obj.is_a]
            else:
                parent = None
            return "" if parent and obj.description == parent.description else obj.description
        return ""

    def _link(
        self,
        obj: Optional[Element],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        """Create a link to ref if appropriate.

        @param ref: the name or value of a class, slot, type or the name of a built in type.
        @param after_link: Text to put between link and description
        @param use_desc: True means append a description after the link if available
        @param add_subset: True means add any subset information that is available
        @return:
        """
        nl = "\n"
        if obj is None or not self.is_secondary_ref(obj.name):
            return self.bbin(obj)
        if isinstance(obj, SlotDefinition):
            link_name = ((be(obj.domain) + "➞") if obj.alias else "") + self.aliased_slot_name(obj)
            link_ref = underscore(obj.name)
        elif isinstance(obj, TypeDefinition):
            link_name = camelcase(obj.name)
            link_ref = f"types/{link_name}" if not self.no_types_dir else f"{link_name}"
        elif isinstance(obj, ClassDefinition):
            link_name = camelcase(obj.name)
            link_ref = camelcase(link_name)
        elif isinstance(obj, SubsetDefinition):
            link_name = camelcase(obj.name)
            link_ref = camelcase(link_name)
        else:
            link_name = obj.name
            link_ref = link_name
        desc = self.desc_for(obj, use_desc)
        return (
            f"[{link_name}]"
            f"({link_ref}.{self.format})"
            + (f" {after_link} " if after_link else "")
            + (f" - {desc.split(nl)[0]}" if desc else "")
        ).rstrip()

    def type_link(
        self,
        ref: Optional[Union[str, TypeDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        return self._link(
            self.schema.types[ref] if isinstance(ref, str) else ref,
            after_link=after_link,
            use_desc=use_desc,
            add_subset=add_subset,
        )

    def slot_link(
        self,
        ref: Optional[Union[str, SlotDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        return self._link(
            self.schema.slots[ref] if isinstance(ref, str) else ref,
            after_link=after_link,
            use_desc=use_desc,
            add_subset=add_subset,
        )

    def class_link(
        self,
        ref: Optional[Union[str, ClassDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        return self._link(
            self.schema.classes[ref] if isinstance(ref, str) else ref,
            after_link=after_link,
            use_desc=use_desc,
            add_subset=add_subset,
        )

    def class_type_link(
        self,
        ref: Optional[Union[str, ClassDefinition, TypeDefinition, EnumDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        if isinstance(ref, ClassDefinition):
            return self.class_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)
        elif isinstance(ref, TypeDefinition):
            return self.type_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)
        elif isinstance(ref, EnumDefinition):
            return self.type_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)
        elif ref in self.schema.classes:
            return self.class_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)
        elif ref in self.schema.enums:
            return self.enum_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)
        else:
            return self.type_link(ref, after_link=after_link, use_desc=use_desc, add_subset=add_subset)

    def enum_link(
        self,
        ref: Optional[Union[str, EnumDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
        add_subset: bool = True,
    ) -> str:
        return self._link(
            self.schema.enums[ref] if isinstance(ref, str) else ref,
            after_link=after_link,
            use_desc=use_desc,
            add_subset=add_subset,
        )

    def subset_link(
        self,
        ref: Optional[Union[str, SubsetDefinition]],
        *,
        after_link: str = None,
        use_desc: bool = False,
    ) -> str:
        return self._link(
            self.schema.subsets[ref] if isinstance(ref, str) else ref,
            after_link=after_link,
            use_desc=use_desc,
        )

    def xlink(self, id_: str) -> str:
        return f"[{id_}]({self.id_to_url(id_)})"

    def exist_warning(self, *fpath: str) -> str:
        """Create a file name out of fpath and check whether it already exists"""
        fname = os.path.join(*fpath)
        if self.warn_on_exist and os.path.exists(fname):
            self.logger.warning(f"File {fname} already exists")
        return fname

    @staticmethod
    def id_to_url(id_: str) -> str:
        uri = id_
        if ":" in id_:
            # TODO! use PC
            if id_.startswith("SIO:"):
                uri = id_.replace("SIO:", "http://semanticscience.org/resource/SIO_")
            elif id_.startswith("HGNC:"):
                uri = "https://monarchinitiative.org/gene/" + id_
            else:
                frag = id_.replace(":", "_")
                base = "http://purl.obolibrary.org/obo/"
                uri = base + frag
        return uri


def pad_heading(text: str) -> str:
    """Add an extra newline to a non-top-level header that doesn't have one preceding it"""
    return re.sub(r"(?<!\n)\n##", "\n\n##", text)


@shared_arguments(MarkdownGenerator)
@click.command(name="markdown")
@click.option("--dir", "-d", required=True, help="Output directory")
@click.option("--classes", "-c", multiple=True, help="Class(es) to emit")
@click.option("--map-fields", "-M", multiple=True, help="Map metamodel fields, e.g. slot=field")
@click.option("--img", "-i", is_flag=True, help="Download YUML images to 'image' directory")
@click.option("--index-file", "-I", help="Name of markdown file that holds index")
@click.option("--noimages", is_flag=True, help="Do not (re-)generate images")
@click.option("--noyuml", is_flag=True, help="Do not add yUML figures to pages")
@click.option("--notypesdir", is_flag=True, help="Do not create a separate types directory")
@click.option("--warnonexist", is_flag=True, help="Warn if output file already exists")
@click.version_option(__version__, "-V", "--version")
def cli(yamlfile, map_fields, dir, img, index_file, notypesdir, warnonexist, **kwargs):
    """Generate markdown documentation of a LinkML model.

    .. warning::
        `gen-markdown` is deprecated. Please use `gen-doc` instead.
    """
    deprecation_warning("gen-markdown")

    gen = MarkdownGenerator(yamlfile, no_types_dir=notypesdir, warn_on_exist=warnonexist, **kwargs)
    if map_fields is not None:
        gen.metamodel_name_map = {}
        for mf in map_fields:
            [k, v] = mf.split("=")
            gen.metamodel_name_map[k] = v
    gen.serialize(directory=dir, image_dir=img, **kwargs)


if __name__ == "__main__":
    cli()
