#include <tuple>
#include <variant>

#include <pybind11/operators.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <pybind11/stl.h>

#include <arbor/morph/morphology.hpp>
#include <arbor/morph/mprovider.hpp>
#include <arbor/morph/region.hpp>
#include <arbor/morph/place_pwlin.hpp>
#include <arbor/morph/primitives.hpp>
#include <arbor/morph/segment_tree.hpp>
#include <arbor/version.hpp>

#include <arborio/label_parse.hpp>
#include <arborio/swcio.hpp>
#include <arborio/neurolucida.hpp>
#include <arborio/neuroml.hpp>
#include <arborio/debug.hpp>

#include "util.hpp"
#include "error.hpp"
#include "label_dict.hpp"
#include "strprintf.hpp"

namespace py = pybind11;

namespace pyarb {

static inline bool cable_lt(const arb::mcable& a, const arb::mcable& b) {
    return std::tuple(a.branch, a.prox_pos, a.dist_pos)<std::tuple(b.branch, b.prox_pos, b.dist_pos);
}

void check_trailing(std::istream& in, std::string fname) {
    if (!(in >> std::ws).eof()) {
        throw pyarb_error(util::pprintf("Trailing data found at end of file '{}'", fname));
    }
}

void register_morphology(py::module& m) {
    using namespace py::literals;

    m.attr("mnpos") = arb::mnpos;

    py::class_<arb::morphology> morph(m, "morphology", "A cell morphology.");
    py::class_<arb::mlocation> location(m, "location", "A location on a cable cell.");
    py::class_<arb::mextent> extent(m, "extent", "A potentially empty region on a morphology.");
    py::class_<arb::mpoint> mpoint(m, "mpoint");
    py::class_<arb::mcable> cable(m, "cable");
    py::class_<arb::isometry> isometry(m, "isometry");
    py::class_<arb::place_pwlin> place(m, "place_pwlin");
    py::class_<arb::segment_tree> segment_tree(m, "segment_tree");
    py::class_<arborio::neuroml> neuroml(m, "neuroml");
    py::class_<arb::mprovider> prov(m, "morphology_provider");
    py::class_<arb::msegment> msegment(m, "msegment");

    py::class_<arborio::nml_metadata> nml_meta(m, "nml_metadata");
    py::class_<arborio::asc_metadata> asc_meta(m,
                                               "asc_metadata",
                                               "Neurolucida metadata type: Spines and marker sets.");
    py::class_<arborio::loaded_morphology> loaded_morphology(m,
                                                             "loaded_morphology",
                                                             "The morphology and label dictionary meta-data loaded from file.");

    py::class_<arborio::swc_metadata> swc_meta(m,
                                               "swc_metadata",
                                               "SWC metadata type: empty.");


    // arb::mlocation
    location
        .def(py::init(
            [](arb::msize_t branch, double pos) {
                const arb::mlocation mloc{branch, pos};
                pyarb::assert_throw(arb::test_invariants(mloc), "invalid location");
                return mloc;
            }),
            "branch"_a, "pos"_a,
            "Construct a location specification holding:\n"
            "  branch:   The id of the branch.\n"
            "  pos:      The relative position (from 0., proximal, to 1., distal) on the branch.\n")
        .def_readonly("branch",  &arb::mlocation::branch,
            "The id of the branch.")
        .def_readonly("pos", &arb::mlocation::pos,
            "The relative position on the branch (∈ [0.,1.], where 0. means proximal and 1. distal).")
        .def(py::self==py::self)
        .def("__str__",
            [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); })
        .def("__repr__",
            [](arb::mlocation l) { return util::pprintf("(location {} {})", l.branch, l.pos); });

    // arb::mpoint
    mpoint
        .def(py::init<double, double, double, double>(),
             "x"_a, "y"_a, "z"_a, "radius"_a,
             "Create an mpoint object from parameters x, y, z, and radius, specified in µm.")
        .def(py::init([](const std::tuple<double, double, double, double>& t) {
                return arb::mpoint{std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t)}; }),
             "Create an mpoint object from a tuple (x, y, z, radius), specified in µm.")
        .def_readonly("x", &arb::mpoint::x, "X coordinate [μm].")
        .def_readonly("y", &arb::mpoint::y, "Y coordinate [μm].")
        .def_readonly("z", &arb::mpoint::z, "Z coordinate [μm].")
        .def_readonly("radius", &arb::mpoint::radius,
            "Radius of cable at sample location centred at coordinates [μm].")
        .def(py::self==py::self)
        .def("__str__",
            [](const arb::mpoint& p) {
                return util::pprintf("<arbor.mpoint: x {}, y {}, z {}, radius {}>", p.x, p.y, p.z, p.radius);
            })
        .def("__repr__",
            [](const arb::mpoint& p) {return util::pprintf("{}", p);});

    py::implicitly_convertible<const std::tuple<double, double, double, double>&, arb::mpoint>();
    py::implicitly_convertible<py::tuple, arb::mpoint>();

    // arb::msegment
    msegment
        .def_readonly("prox", &arb::msegment::prox, "the location and radius of the proximal end.")
        .def_readonly("dist", &arb::msegment::dist, "the location and radius of the distal end.")
        .def_readonly("tag", &arb::msegment::tag, "tag meta-data.");

    // arb::mcable
    cable
        .def(py::init(
            [](arb::msize_t bid, double prox, double dist) {
                arb::mcable c{bid, prox, dist};
                if (!test_invariants(c)) {
                    throw pyarb_error("Invalid cable description. Cable segments must have proximal and distal end points in the range [0,1].");
                }
                return c;
            }),
            "branch"_a, "prox"_a, "dist"_a)
        .def_readonly("branch", &arb::mcable::branch,
                "The id of the branch on which the cable lies.")
        .def_readonly("prox", &arb::mcable::prox_pos,
                "The relative position of the proximal end of the cable on its branch ∈ [0,1].")
        .def_readonly("dist", &arb::mcable::dist_pos,
                "The relative position of the distal end of the cable on its branch ∈ [0,1].")
        .def(py::self==py::self)
        .def("__str__", [](const arb::mcable& c) { return util::pprintf("{}", c); })
        .def("__repr__", [](const arb::mcable& c) { return util::pprintf("{}", c); });

    // arb::isometry
    isometry
        .def(py::init<>(), "Construct a trivial isometry.")
        .def("__call__", [](arb::isometry& iso, arb::mpoint& p) {
                return iso.apply(p);
            },
            "Apply isometry to mpoint argument.")
        .def("__call__", [](arb::isometry& iso, py::tuple t) {
                int len = py::len(t);
                if (len<3) throw std::runtime_error("tuple length < 3");

                arb::mpoint p{t[0].cast<double>(), t[1].cast<double>(), t[2].cast<double>(), 0.};
                p = iso.apply(p);

                py::tuple result(len);
                result[0] = p.x;
                result[1] = p.y;
                result[2] = p.z;
                for (int i = 3; i<len; ++i) {
                    result[i] = t[i];
                }
                return result;
            },
            "Apply isometry to first three components of tuple argument.")
        .def(py::self*py::self)
        .def_static("translate",
            [](double x, double y, double z) { return arb::isometry::translate(x, y, z); },
            "x"_a, "y"_a, "z"_a,
            "Construct a translation isometry from displacements x, y, and z.")
        .def_static("translate",
            [](py::tuple t) {
                if (py::len(t)!=3) throw std::runtime_error("tuple length != 3");
                return arb::isometry::translate(t[0].cast<double>(), t[1].cast<double>(), t[2].cast<double>());
            },
            "Construct a translation isometry from the first three components of a tuple.")
        .def_static("translate",
            [](arb::mpoint p) { return arb::isometry::translate(p.x, p.y, p.z); },
            "Construct a translation isometry from the x, y, and z components of an mpoint.")
        .def_static("rotate",
            [](double theta, double x, double y, double z) { return arb::isometry::rotate(theta, x, y, z); },
            "theta"_a, "x"_a, "y"_a, "z"_a,
            "Construct a rotation isometry of angle theta about the axis in direction (x, y, z).")
        .def_static("rotate",
            [](double theta, py::tuple t) {
                if (py::len(t)!=3) throw std::runtime_error("tuple length != 3");
                return arb::isometry::rotate(theta, t[0].cast<double>(), t[1].cast<double>(), t[2].cast<double>());
            },
            "theta"_a, "axis"_a,
            "Construct a rotation isometry of angle theta about the given axis in the direction described by a tuple.");

    // arb::place_pwlin
    place
        .def(py::init<const arb::morphology&, const arb::isometry&>(),
            "morphology"_a, py::arg_v("isometry", arb::isometry(), "id"),
            "Construct a piecewise-linear placement object from the given morphology and optional isometry.")
        .def("at", &arb::place_pwlin::at, "location"_a,
            "Return an interpolated mpoint corresponding to the location argument.")
        .def("all_at", &arb::place_pwlin::all_at, "location"_a,
            "Return list of all possible interpolated mpoints corresponding to the location argument.")
        .def("segments",
            [](const arb::place_pwlin& self, std::vector<arb::mcable> cables) {
                std::sort(cables.begin(), cables.end(), cable_lt);
                return self.segments(cables);
            },
            "Return minimal list of full or partial msegments whose union is coterminous "
            "with the extent of the given list of cables.")
        .def("all_segments",
            [](const arb::place_pwlin& self, std::vector<arb::mcable> cables) {
                std::sort(cables.begin(), cables.end(), cable_lt);
                return self.all_segments(cables);
            },
            "Return maximal list of non-overlapping full or partial msegments whose union is coterminous "
            "with the extent of the given list of cables.")
        .def("closest",
            [](const arb::place_pwlin& self, double x, double y, double z) {
                auto [l, d] = self.closest(x, y, z);
                return pybind11::make_tuple(l, d);
            },
            "Find the location on the morphology that is closest to a 3d point. "
            "Returns the location and its distance from the point.");

    // arb::place_pwlin
    prov
        .def(py::init<const arb::morphology&>(),
            "morphology"_a,
            "Construct a morphology provider.")
        .def("reify_locset",
             [](const arb::mprovider& p,
                const std::string& r) {
                 return thingify(arborio::parse_locset_expression(r).unwrap(), p);
             },
             "Turn a locset into a list of locations.")
        .def("reify_region",
             [](const arb::mprovider& p,
                const std::string& r) {
                 return thingify(arborio::parse_region_expression(r).unwrap(), p);
             },
             "Turn a region into an extent.");

    // arb::segment_tree
    segment_tree
        // constructors
        .def(py::init<>())
        // modifiers
        .def("reserve", &arb::segment_tree::reserve)
        .def("append", [](arb::segment_tree& t, arb::msize_t parent, arb::mpoint prox, arb::mpoint dist, int tag) {
                            return t.append(parent, prox, dist, tag);
                          },
                "parent"_a, "prox"_a, "dist"_a, "tag"_a,
                "Append a segment to the tree.")
        .def("append", [](arb::segment_tree& t, arb::msize_t parent, arb::mpoint dist, int tag) {
                            return t.append(parent, dist, tag);
                          },
                "parent"_a, "dist"_a, "tag"_a,
                "Append a segment to the tree.")
        .def("append",
                [](arb::segment_tree& t, arb::msize_t p, double x, double y, double z, double radius, int tag) {
                    return t.append(p, arb::mpoint{x,y,z,radius}, tag);
                },
                "parent"_a, "x"_a, "y"_a, "z"_a, "radius"_a, "tag"_a,
                "Append a segment to the tree, using the distal location of the parent segment as the proximal end.")
        .def("is_fork", &arb::segment_tree::is_fork,
                "i"_a, "True if segment has more than one child.")
        .def("is_terminal", &arb::segment_tree::is_terminal,
                "i"_a, "True if segment has no children.")
        .def("is_root", &arb::segment_tree::is_root,
                "i"_a, "True if segment has no parent.")
        // properties
        .def_property_readonly("empty", [](const arb::segment_tree& st){return st.empty();},
                "Indicates whether the tree is empty (i.e. whether it has size 0)")
        .def_property_readonly("size", [](const arb::segment_tree& st){return st.size();},
                "The number of segments in the tree.")
        .def_property_readonly("parents", [](const arb::segment_tree& st){return st.parents();},
                "A list with the parent index of each segment.")
        .def_property_readonly("segments", [](const arb::segment_tree& st){return st.segments();},
                "A list of the segments.")
        .def("apply_isometry",
             [](const arb::segment_tree& t, const arb::isometry& i) { return arb::apply(t, i); },
             "Apply an isometry to all segments in the tree.")
        .def("split_at",
             [](const arb::segment_tree& t, arb::msize_t id) { return arb::split_at(t, id); },
             "Split into a pair of trees at the given id, such that one tree is the subtree rooted at id and the other is the original tree without said subtree.")
        .def("join_at",
             [](const arb::segment_tree& t, arb::msize_t id, const arb::segment_tree& o) { return arb::join_at(t, id, o); },
             "Join two subtrees at a given id, such that said id becomes the parent of the inserted sub-tree.")
        .def("equivalent",
             [](const arb::segment_tree& t, const arb::segment_tree& o) { return arb::equivalent(t, o); },
             "Two trees are equivalent, but not neccessarily identical, ie they have the same segments and structure.")
        .def("tag_roots",
            [](const arb::segment_tree& t, int tag) { return arb::tag_roots(t, tag); },
            "Get roots of tag region of this segment tree.")
        .def("show",
             [] (const arb::segment_tree& t) { return arborio::show(t); },
             "Return an ASCII representation of this segment tree.")
        .def("__str__", [](const arb::segment_tree& s) {
                return util::pprintf("<arbor.segment_tree:\n{}>", s);});

    // arb::morphology
    morph
        // constructors
        .def(py::init(
                [](arb::segment_tree t){
                    return arb::morphology(std::move(t));
                }))
        // morphology's interface is read-only by design, so most of it can
        // be implemented as read-only properties.
        .def_property_readonly("empty",
                [](const arb::morphology& m){return m.empty();},
                "Whether the morphology is empty.")
        .def_property_readonly("num_branches",
                [](const arb::morphology& m){return m.num_branches();},
                "The number of branches in the morphology.")
        .def("branch_parent", &arb::morphology::branch_parent,
                "i"_a, "The parent branch of branch i.")
        .def("branch_children", &arb::morphology::branch_children,
                "i"_a, "The child branches of branch i.")
        .def("branch_segments",
                [](const arb::morphology& m, arb::msize_t i) {
                    return m.branch_segments(i);
                },
                "i"_a, "A list of the segments in branch i, ordered from proximal to distal ends of the branch.")
        .def("to_segment_tree", &arb::morphology::to_segment_tree,
                "Convert this morphology to a segment_tree.")
        .def("show",
             [] (const arb::morphology& t) { return arborio::show(t); },
             "Return an ASCII representation.")
        .def("__str__",
                [](const arb::morphology& m) {
                    return util::pprintf("<arbor.morphology:\n{}>", m);
                });

    py::implicitly_convertible<arb::segment_tree, arb::morphology>();

    // Function that creates a morphology/segment_tree from an swc file.
    // Wraps calls to C++ functions arborio::parse_swc() and arborio::load_swc_arbor().
    m.def("load_swc_arbor",
        [](py::object fn) -> arborio::loaded_morphology {
            try {
                auto contents = util::read_file_or_buffer(fn);
                auto data = arborio::parse_swc(contents);
                return arborio::load_swc_arbor(data);
            }
            catch (arborio::swc_error& e) {
                // Try to produce helpful error messages for SWC parsing errors.
                throw pyarb_error(util::pprintf("Arbor SWC: parse error: {}", e.what()));
            }
        },
        "filename_or_stream"_a,
        "Generate a morphology/segment_tree from an SWC file following the rules prescribed by Arbor.\n"
        "Specifically:\n"
        " * Single-segment somas are disallowed.\n"
        " * There are no special rules related to somata. They can be one or multiple branches\n"
        "   and other segments can connect anywhere along them.\n"
        " * A segment is always created between a sample and its parent, meaning there\n"
        "   are no gaps in the resulting morphology.");
    m.def("load_swc_neuron",
        [](py::object fn) -> arborio::loaded_morphology {
            try {
                auto contents = util::read_file_or_buffer(fn);
                auto data = arborio::parse_swc(contents);
                return arborio::load_swc_neuron(data);
            }
            catch (arborio::swc_error& e) {
                // Try to produce helpful error messages for SWC parsing errors.
                throw pyarb_error(util::pprintf("NEURON SWC: parse error: {}", e.what()));
            }
        },
        "filename_or_stream"_a,
        "Generate a morphology from an SWC file following the rules prescribed by NEURON.\n"
        "See the documentation https://docs.arbor-sim.org/en/latest/fileformat/swc.html\n"
        "for a detailed description of the interpretation.");

    // Neurolucida ASCII, or .asc, file format support.
    py::class_<arborio::asc_color> color(m,
                                         "asc_color",
                                         "Neurolucida color tag.");
    color
        .def_readonly("red", &arborio::asc_color::r)
        .def_readonly("blue", &arborio::asc_color::b)
        .def_readonly("green", &arborio::asc_color::g);


    py::class_<arborio::asc_spine> spine(m,
                                         "asc_spine",
                                         "Neurolucida spine marker.");
    spine
        .def_readonly("name", &arborio::asc_spine::name)
        .def_readonly("location", &arborio::asc_spine::location);


    py::enum_<arborio::asc_marker> marker(m,
                                           "asc_marker",
                                           "Neurolucida marker type.");
    marker
        .value("dot", arborio::asc_marker::dot)
        .value("cross", arborio::asc_marker::cross)
        .value("circle", arborio::asc_marker::circle)
        .value("none", arborio::asc_marker::none);

    py::class_<arborio::asc_marker_set> marker_set(m,
                                                  "asc_marker_set",
                                                  "Neurolucida marker set type.");

    marker_set
        .def_readonly("name", &arborio::asc_marker_set::name)
        .def_readonly("marker", &arborio::asc_marker_set::marker)
        .def_readonly("color", &arborio::asc_marker_set::color)
        .def_readonly("locations", &arborio::asc_marker_set::locations);

    asc_meta
        .def_readonly("markers", &arborio::asc_metadata::markers)
        .def_readonly("spines", &arborio::asc_metadata::spines);

    loaded_morphology
        .def_readonly("morphology",
                &arborio::loaded_morphology::morphology,
                "The cable cell morphology.")
        .def_readonly("segment_tree",
                &arborio::loaded_morphology::segment_tree,
                "The raw segment tree.")
        .def_readonly("metadata",
                &arborio::loaded_morphology::metadata,
                "File type specific metadata.")
        .def_property_readonly("labels",
            [](const arborio::loaded_morphology& m) {return label_dict_proxy(m.labels);},
            "Any labels defined by the loaded file.");

    m.def("load_asc",
        [](py::object fn) -> arborio::loaded_morphology {
            try {
                auto contents = util::read_file_or_buffer(fn);
                return arborio::parse_asc_string(contents.c_str());
            }
            catch (std::exception& e) {
                // Try to produce helpful error messages for SWC parsing errors.
                throw pyarb_error(util::pprintf("error loading neurolucida asc file: {}", e.what()));
            }
        },
        "filename_or_stream"_a,
        "Load a morphology or segment_tree and meta data from a Neurolucida ASCII .asc file.");

    // arborio::morphology_data
    nml_meta
        .def_readonly("cell_id",
            &arborio::nml_metadata::cell_id,
            "Cell id, or empty if morphology was taken from a top-level <morphology> element.")
        .def_readonly("id",
            &arborio::nml_metadata::id,
            "Morphology id.")
        .def("segments",
            [](const arborio::nml_metadata& md) {return label_dict_proxy(md.segments);},
            "Label dictionary containing one region expression for each segment id.")
        .def("named_segments",
            [](const arborio::nml_metadata& md) {return label_dict_proxy(md.named_segments);},
            "Label dictionary containing one region expression for each name applied to one or more segments.")
        .def("groups",
            [](const arborio::nml_metadata& md) {return label_dict_proxy(md.groups);},
            "Label dictionary containing one region expression for each segmentGroup id.")
        .def_readonly("group_segments",
            &arborio::nml_metadata::group_segments,
            "Map from segmentGroup ids to their corresponding segment ids.");

    // arborio::neuroml
    neuroml
        // constructors
        .def(py::init(
            [](py::object fn) {
                try {
                    auto contents = util::read_file_or_buffer(fn);
                    return arborio::neuroml(contents);
                }
                catch (arborio::neuroml_exception& e) {
                    // Try to produce helpful error messages for NeuroML parsing errors.
                    throw pyarb_error(util::pprintf("NeuroML error: {}", e.what()));
                }
            }),
            "Construct NML morphology from filename or stream.")
        .def("cell_ids",
            [](const arborio::neuroml& nml) {
                try {
                    return nml.cell_ids();
                }
                catch (arborio::neuroml_exception& e) {
                    throw util::pprintf("NeuroML error: {}", e.what());
                }
            },
            "Query top-level cells.")
        .def("morphology_ids",
            [](const arborio::neuroml& nml) {
                try {
                    return nml.morphology_ids();
                }
                catch (arborio::neuroml_exception& e) {
                    throw util::pprintf("NeuroML error: {}", e.what());
                }
            },
            "Query top-level standalone morphologies.")
        .def("morphology",
            [](const arborio::neuroml& nml, const std::string& morph_id, bool spherical) {
                try {
                    using namespace arborio::neuroml_options;
                    return nml.morphology(morph_id, spherical? allow_spherical_root: none);
                }
                catch (arborio::neuroml_exception& e) {
                    throw util::pprintf("NeuroML error: {}", e.what());
                }
            }, "morph_id"_a, "allow_spherical_root"_a=false,
            "Retrieve top-level nml_morph_data associated with morph_id.")
        .def("cell_morphology",
            [](const arborio::neuroml& nml, const std::string& cell_id, bool spherical) {
                try {
                    using namespace arborio::neuroml_options;
                    return nml.cell_morphology(cell_id, spherical? allow_spherical_root: none);
                }
                catch (arborio::neuroml_exception& e) {
                    throw util::pprintf("NeuroML error: {}", e.what());
                }
            }, "cell_id"_a, "allow_spherical_root"_a=false,
            "Retrieve nml_morph_data associated with cell_id.");
}

} // namespace pyarb
