""" Load WKB into PySAL shapes.

Where PySAL shapes support multiple parts, "MULTI"type shapes
will be converted to a single multi-part shape:
    
    MULTIPOLYGON -> Polygon
    MULTILINESTRING -> Chain

Otherwise a list of shapes will be returned:
    
    MULTIPOINT -> [pt0, ..., ptN]

Some concepts aren't well supported by PySAL shapes. For example:
    
    wkt = 'MULTIPOLYGON EMPTY' -> '\x01   \x06\x00\x00\x00   \x00\x00\x00\x00'
                                  |  <  | WKBMultiPolygon |    0 parts      |
    
``pysal.cg.Polygon`` does not support 0 part polygons. ``None`` is returned in this case.


REFERENCE MATERIAL:
SOURCE: http://webhelp.esri.com/arcgisserver/9.3/dotNet/index.htm#geodatabases/the_ogc_103951442.htm

    Basic Type definitions
    byte : 1 byte
    uint32 : 32 bit unsigned integer  (4 bytes)
    double : double precision number (8 bytes)

    Building Blocks : Point, LinearRing

"""

from io import StringIO
from ... import cg
import sys
import struct

__author__ = "Charles R Schmidt <schmidtc@gmail.com>"
__all__ = ["loads"]

"""
enum wkbByteOrder {
    wkbXDR = 0,              Big Endian
    wkbNDR = 1               Little Endian
};
"""


DEFAULT_ENDIAN = "<" if sys.byteorder == "little" else ">"
ENDIAN = {"\x00": ">", "\x01": "<"}


def load_ring_little(dat) -> list:
    """
    LinearRing   {
        uint32  numPoints;
        Point   points[numPoints];
    }
    """
    npts = struct.unpack("<I", dat.read(4))[0]
    xy = struct.unpack("<%dd" % (npts * 2), dat.read(npts * 2 * 8))
    return [cg.Point(xy[i : i + 2]) for i in range(0, npts * 2, 2)]


def load_ring_big(dat) -> list:
    npts = struct.unpack(">I", dat.read(4))[0]
    xy = struct.unpack(">%dd" % (npts * 2), dat.read(npts * 2 * 8))
    return [cg.Point(xy[i : i + 2]) for i in range(0, npts * 2, 2)]


def loads(s: str):
    """
    WKBGeometry  {
        union {
            WKBPoint                        point;
            WKBLineString                   linestring;
            WKBPolygon                      polygon;
            WKBGeometryCollection           collection;
            WKBMultiPoint                   mpoint;
            WKBMultiLineString              mlinestring;
            WKBMultiPolygon                 mpolygon;
        }
    };
    
    Returns
    -------
    geom : {None, libpysal.cg.{Point, Chain, Polygon}}
        The geometric object or ``None``.
    
    Raises
    ------
    TypeError
        Raised when an unsupported shape type is passed in.
    
    """

    # To allow recursive calls, read only the bytes we need.
    if hasattr(s, "read"):
        dat = s
    else:
        dat = StringIO(s)
    endian = ENDIAN[dat.read(1)]
    typ = struct.unpack("I", dat.read(4))[0]
    if typ == 1:
        """
        WKBPoint {
            byte                byteOrder;
            uint32              wkbType;                        1
            Point               point;
        }
        Point {
            double x;
            double y;
        };
        """
        x, y = struct.unpack(endian + "dd", dat.read(16))
        geom = cg.Point((x, y))

    elif typ == 2:
        """
        WKBLineString {
            byte                byteOrder;
            uint32              wkbType;                        2
            uint32              numPoints;
            Point               points[numPoints];
        }
        """
        n = struct.unpack(endian + "I", dat.read(4))[0]
        xy = struct.unpack(endian + "%dd" % (n * 2), dat.read(n * 2 * 8))
        geom = cg.Chain([cg.Point(xy[i : i + 2]) for i in range(0, n * 2, 2)])

    elif typ == 3:
        """
        WKBPolygon  {
            byte                byteOrder;
            uint32              wkbType;                        3
            uint32              numRings;
            LinearRing          rings[numRings];
        }

        WKBPolygon has exactly 1 outer ring and `n` holes.
        Multipart Polygons are NOT support by WKBPolygon.
        """
        nrings = struct.unpack(endian + "I", dat.read(4))[0]
        load_ring = load_ring_little if endian == "<" else load_ring_big
        rings = [load_ring(dat) for _ in range(nrings)]
        geom = cg.Polygon(rings[0], rings[1:])
    elif typ == 4:
        """
        WKBMultiPoint   {
            byte                byteOrder;
            uint32              wkbType;                        4
            uint32              num_wkbPoints;
            WKBPoint            WKBPoints[num_wkbPoints];
        }
        """
        npts = struct.unpack(endian + "I", dat.read(4))[0]
        geom = [loads(dat) for _ in range(npts)]

    elif typ == 5:
        """
        WKBMultiLineString  {
            byte                byteOrder;
            uint32              wkbType;                        5
            uint32              num_wkbLineStrings;
            WKBLineString       WKBLineStrings[num_wkbLineStrings];
        }
        """
        nparts = struct.unpack(endian + "I", dat.read(4))[0]
        chains = [loads(dat) for _ in range(nparts)]
        geom = cg.Chain(sum([c.parts for c in chains], []))

    elif typ == 6:
        """
        wkbMultiPolygon {
            byte                byteOrder;
            uint32              wkbType;                        6
            uint32              num_wkbPolygons;
            WKBPolygon          wkbPolygons[num_wkbPolygons];
        }

        """
        npolys = struct.unpack(endian + "I", dat.read(4))[0]
        polys = [loads(dat) for _ in range(npolys)]
        parts = sum([p.parts for p in polys], [])
        holes = sum([p.holes for p in polys if p.holes[0]], [])

        # MULTIPOLYGON EMPTY, isn't well supported by PySAL shape types.
        if not parts:
            geom = None
        else:
            geom = cg.Polygon(parts, holes)
    elif typ == 7:
        """
        WKBGeometryCollection {
            byte                byte_order;
            uint32              wkbType;                        7
            uint32              num_wkbGeometries;
            WKBGeometry         wkbGeometries[num_wkbGeometries]
        }
        """
        ngeoms = struct.unpack(endian + "I", dat.read(4))[0]
        geom = [loads(dat) for _ in range(ngeoms)]

    try:
        return geom
    except NameError:
        raise TypeError("Type (%d) is unknown or unsupported." % typ)


if __name__ == "__main__":

    # TODO: Refactor below into Unit Tests
    wktExamples = [
        "POINT(6 10)",
        "LINESTRING(3 4,10 50,20 25)",
        "POLYGON((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2))",
        "MULTIPOINT(3.5 5.6,4.8 10.5)",
        "MULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))",
        # This MULTIPOLYGON is not valid, the 2nd shell instects the 1st.
        #'MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2)),((3 3,6 2,6 4,3 3)))',
        "MULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2, 3 2, 3 3, 2 3,2 2)),((5 3,6 2,6 4,5 3)))",
        "GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))",
        #'POINT ZM (1 1 5 60)',  <-- ZM is not supported by WKB ?
        #'POINT M (1 1 80)',     <-- M is not supported by WKB ?
        #'POINT EMPTY',          <-- NOT SUPPORT
        "MULTIPOLYGON EMPTY",
    ]

    # shapely only used for testing.
    try:
        import shapely.wkt, shapely.geometry
        from pysal.contrib.shapely_ext import to_wkb
    except ImportError:
        print("shapely is used to test this module.")
        raise
    for example in wktExamples:
        print(example)
        shape0 = shapely.wkt.loads(example)
        shape1 = loads(shape0.to_wkb())
        if example.startswith("MULTIPOINT"):
            shape2 = shapely.geometry.asMultiPoint(shape1)
        elif example.startswith("GEOMETRYCOLLECTION"):
            shape2 = shapely.geometry.collection.GeometryCollection(
                list(map(shapely.geometry.asShape, shape1))
            )
        elif example == "MULTIPOLYGON EMPTY":
            # Skip Test
            shape2 = None
        else:
            shape2 = shapely.geometry.asShape(shape1)

        print(shape1)
        if shape2:
            assert shape0.equals(shape2)
            print(shape0.equals(shape2))
        else:
            print("Skip")

        print("")
