#
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""DAG demonstrating various options for a trigger form generated by DAG params.

The DAG attribute `params` is used to define a default dictionary of parameters which are usually passed
to the DAG and which are used to render a trigger form.
"""
from __future__ import annotations

import datetime
import json
from pathlib import Path
from typing import TYPE_CHECKING

from airflow import DAG
from airflow.decorators import task
from airflow.exceptions import AirflowSkipException
from airflow.models.param import Param

if TYPE_CHECKING:
    from airflow.models.dagrun import DagRun
    from airflow.models.taskinstance import TaskInstance

with DAG(
    dag_id=Path(__file__).stem,
    description=__doc__.partition(".")[0],
    doc_md=__doc__,
    schedule=None,
    start_date=datetime.datetime(2022, 3, 4),
    catchup=False,
    tags=["example_ui"],
    params={
        # Let's start simple: Standard dict values are detected from type and offered as entry form fields.
        # Detected types are numbers, text, boolean, lists and dicts.
        # Note that such auto-detected parameters are treated as optional (not required to contain a value)
        "x": 3,
        "text": "Hello World!",
        "flag": False,
        "a_simple_list": ["one", "two", "three", "actually one value is made per line"],
        # But of course you might want to have it nicer! Let's add some description to parameters.
        # Note if you can add any HTML formatting to the description, you need to use the description_html
        # attribute.
        "most_loved_number": Param(
            42,
            type="integer",
            title="Your favorite number",
            description_html="""Everybody should have a favorite number. Not only math teachers.
            If you can not think of any at the moment please think of the 42 which is very famous because
            of the book
            <a href='https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#
            The_Answer_to_the_Ultimate_Question_of_Life,_the_Universe,_and_Everything_is_42'>
            The Hitchhiker's Guide to the Galaxy</a>""",
        ),
        # If you want to have a selection list box then you can use the enum feature of JSON schema
        "pick_one": Param(
            "value 42",
            type="string",
            title="Select one Value",
            description="You can use JSON schema enum's to generate drop down selection boxes.",
            enum=[f"value {i}" for i in range(16, 64)],
        ),
        # You can also label the selected values via values_display attribute
        "pick_with_label": Param(
            3,
            type="number",
            title="Select one Number",
            description="With drop down selections you can also have nice display labels for the values.",
            enum=[*range(1, 10)],
            values_display={
                1: "One",
                2: "Two",
                3: "Three",
                4: "Four - is like you take three and get one for free!",
                5: "Five",
                6: "Six",
                7: "Seven",
                8: "Eight",
                9: "Nine",
            },
        ),
        # If you want to have a list box with proposals but not enforcing a fixed list
        # then you can use the examples feature of JSON schema
        "proposals": Param(
            "some value",
            type="string",
            title="Field with proposals",
            description="You can use JSON schema examples's to generate drop down selection boxes "
            "but allow also to enter custom values. Try typing an 'a' and see options.",
            examples=(
                "Alpha,Bravo,Charlie,Delta,Echo,Foxtrot,Golf,Hotel,India,Juliett,Kilo,Lima,Mike,November,Oscar,Papa,"
                "Quebec,Romeo,Sierra,Tango,Uniform,Victor,Whiskey,X-ray,Yankee,Zulu"
            ).split(","),
        ),
        # If you want to select multiple items from a fixed list JSON schema des not allow to use enum
        # In this case the type "array" is being used together with "examples" as pick list
        "multi_select": Param(
            ["two", "three"],
            "Select from the list of options.",
            type="array",
            title="Multi Select",
            examples=["one", "two", "three", "four", "five"],
        ),
        # A multiple options selection can also be combined with values_display
        "multi_select_with_label": Param(
            ["2", "3"],
            "Select from the list of options. See that options can have nicer text and still technical values"
            "are propagated as values during trigger to the DAG.",
            type="array",
            title="Multi Select with Labels",
            examples=["1", "2", "3", "4", "5"],
            values_display={
                "1": "One box of choccolate",
                "2": "Two bananas",
                "3": "Three apples",
                # Note: Value display mapping does not need to be complete.s
            },
        ),
        # An array of numbers
        "array_of_numbers": Param(
            [1, 2, 3],
            "Only integers are accepted in this array",
            type="array",
            title="Array of numbers",
            items={"type": "number"},
        ),
        # Boolean as proper parameter with description
        "bool": Param(
            True,
            type="boolean",
            title="Please confirm",
            description="A On/Off selection with a proper description.",
        ),
        # Dates and Times are also supported
        "date_time": Param(
            f"{datetime.date.today()}T{datetime.time(hour=12, minute=17, second=00)}+00:00",
            type="string",
            format="date-time",
            title="Date-Time Picker",
            description="Please select a date and time, use the button on the left for a pup-up calendar.",
        ),
        "date": Param(
            f"{datetime.date.today()}",
            type="string",
            format="date",
            title="Date Picker",
            description="Please select a date, use the button on the left for a pup-up calendar. "
            "See that here are no times!",
        ),
        "time": Param(
            f"{datetime.time(hour=12, minute=13, second=14)}",
            type=["string", "null"],
            format="time",
            title="Time Picker",
            description="Please select a time, use the button on the left for a pup-up tool.",
        ),
        # Fields can be required or not. If the defined fields are typed they are getting required by default
        # (else they would not pass JSON schema validation) - to make typed fields optional you must
        # permit the optional "null" type
        "required_field": Param(
            "You can not trigger if no text is given here!",
            type="string",
            title="Required text field",
            description="This field is required. You can not submit without having text in here.",
        ),
        "optional_field": Param(
            "optional text, you can trigger also w/o text",
            type=["null", "string"],
            title="Optional text field",
            description_html="This field is optional. As field content is JSON schema validated you must "
            "allow the <code>null</code> type.",
        ),
        # You can arrange the entry fields in sections so that you can have a better overview for the user
        # Therefore you can add the "section" attribute.
        # The benefit of the Params class definition is that the full scope of JSON schema validation
        # can be leveraged for form fields and they will be validated before DAG submission.
        "checked_text": Param(
            "length-checked-field",
            type="string",
            title="Text field with length check",
            description_html="""This field is required. And you need to provide something between 10 and 30
            characters. See the
            <a href='https://json-schema.org/understanding-json-schema/reference/string.html'>
            JSON schema description (string)</a> in for more details""",
            minLength=10,
            maxLength=20,
            section="JSON Schema validation options",
        ),
        "checked_number": Param(
            100,
            type="number",
            title="Number field with value check",
            description_html="""This field is required. You need to provide any number between 64 and 128.
            See the <a href='https://json-schema.org/understanding-json-schema/reference/numeric.html'>
            JSON schema description (numbers)</a> in for more details""",
            minimum=64,
            maximum=128,
            section="JSON Schema validation options",
        ),
        # Some further cool stuff as advanced options are also possible
        # You can have the user entering a dict object as a JSON with validation
        "object": Param(
            {"key": "value"},
            type=["object", "null"],
            title="JSON entry field",
            section="Special advanced stuff with form fields",
        ),
        "array_of_objects": Param(
            [{"name": "account_name", "country": "country_name"}],
            "Array with complex objects and validation rules. "
            "See <a href='https://json-schema.org/understanding-json-schema"
            "/reference/array.html#items'>JSON Schema validation options in specs.</a>",
            type="array",
            title="JSON array field",
            items={
                "type": "object",
                "properties": {"name": {"type": "string"}, "country_name": {"type": "string"}},
                "required": ["name"],
            },
            section="Special advanced stuff with form fields",
        ),
        # If you want to have static parameters which are always passed and not editable by the user
        # then you can use the JSON schema option of passing constant values. These parameters
        # will not be displayed but passed to the DAG
        "hidden_secret_field": Param("constant value", const="constant value"),
        # Finally besides the standard provided field generator you can have you own HTML form code
        # injected - but be careful, you can also mess-up the layout!
        "color_picker": Param(
            "#FF8800",
            type="string",
            title="Pick a color",
            description_html="""This is a special HTML widget as custom implementation in the DAG code.
            It is templated with the following parameter to render proper HTML form fields:
            <ul>
                <li><code>{name}</code>: Name of the HTML input field that is expected.</li>
                <li><code>{value}</code>:
                    (Default) value that should be displayed when showing/loading the form.</li>
                <li>Note: If you have elements changing a value, call <code>updateJSONconf()</code> to update
                    the form data to be posted as <code>dag_run.conf</code>.</li>
            </ul>
            Example: <code>&lt;input name='{name}' value='{value}' onchange='updateJSONconf()' /&gt;</code>
            """,
            custom_html_form="""
            <table width="100%" cellspacing="5"><tbody><tr><td>
                <label for="r_{name}">Red:</label>
            </td><td width="80%">
                <input id="r_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
            </td><td rowspan="3" style="padding-left: 10px;">
                <div id="preview_{name}"
                style="line-height: 40px; margin-bottom: 7px; width: 100%; background-color: {value};"
                >&nbsp;</div>
                <input class="form-control" type="text" maxlength="7" id="{name}" name="{name}"
                value="{value}" onchange="v_{name}()" />
            </td></tr><tr><td>
                <label for="g_{name}">Green:</label>
            </td><td>
                <input id="g_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
            </td></tr><tr><td>
                <label for="b_{name}">Blue:</label>
            </td><td>
                <input id="b_{name}" type="range" min="0" max="255" value="0" onchange="u_{name}()"/>
            </td></tr></tbody></table>
            <script lang="javascript">
                const hex_chars = "0123456789ABCDEF";
                function i2hex(name) {
                    var i = document.getElementById(name).value;
                    return hex_chars.substr(parseInt(i / 16), 1) + hex_chars.substr(parseInt(i % 16), 1)
                }
                function u_{name}() {
                    var hex_val = "#"+i2hex("r_{name}")+i2hex("g_{name}")+i2hex("b_{name}");
                    document.getElementById("{name}").value = hex_val;
                    document.getElementById("preview_{name}").style.background = hex_val;
                    updateJSONconf();
                }
                function hex2i(text) {
                    return hex_chars.indexOf(text.substr(0,1)) * 16 + hex_chars.indexOf(text.substr(1,1));
                }
                function v_{name}() {
                    var value = document.getElementById("{name}").value.toUpperCase();
                    document.getElementById("r_{name}").value = hex2i(value.substr(1,2));
                    document.getElementById("g_{name}").value = hex2i(value.substr(3,2));
                    document.getElementById("b_{name}").value = hex2i(value.substr(5,2));
                    document.getElementById("preview_{name}").style.background = value;
                }
                v_{name}();
            </script>""",
            section="Special advanced stuff with form fields",
        ),
    },
) as dag:

    @task(task_id="show_params")
    def show_params(**kwargs) -> None:
        ti: TaskInstance = kwargs["ti"]
        dag_run: DagRun = ti.dag_run
        if not dag_run.conf:
            print("Uups, no parameters supplied as DagRun.conf, was the trigger w/o form?")
            raise AirflowSkipException("No DagRun.conf parameters supplied.")
        print(f"This DAG was triggered with the following parameters:\n{json.dumps(dag_run.conf, indent=4)}")

    show_params()
