(function (window, undefined) {

    var SHAPE_TYPES = {
        "0": "Null Shape",
        "1": "Point", // standard shapes
        "3": "PolyLine",
        "5": "Polygon",
        "8": "MultiPoint",
        "11": "PointZ", // 3d shapes
        "13": "PolyLineZ",
        "15": "PolygonZ",
        "18": "MultiPointZ",
        "21": "PointM", // user-defined measurement shapes
        "23": "PolyLineM",
        "25": "PolygonM",
        "28": "MultiPointM",
        "31": "MultiPatch"
    }

    var Shapefile = function (o, callback) {
        var o = typeof o == "string" ? { shp: o } : o
        this.callback = callback

        if (!!o.shp.lastModifiedDate)
            this.handleFile(o);
        else
            this.handleUri(o);
    }

    Shapefile.prototype = {
        constructor: Shapefile,
        handleUri: function (shapefiles) {
            var that = this;

            var file = shapefiles.shp;
            var reader = new FileReader();

            this.url = shapefiles.shp
            this.stream = new Gordon.Stream(reader.readAsText(file))

            this.readFileHeader()
            this.readRecords()
            this.formatIntoGeoJson()

            if (shapefiles.dbf)
                new DBF(shapefiles.dbf, function (data) {
                    that.addDBFDataToGeoJSON(data)
                    that._postMessage()
                });
            else this._postMessage()

        },
        handleFile: function (files) {
            this.options = files
            var that = this;

            if (!!window.FileReader) {
                var reader = new FileReader();
            } else {
                var reader = new FileReaderSync();
            }
            var thisObj = this;
            reader.onload = (e) => {
                thisObj.onFileLoad(e.target.result);
                if(!files.dbf){
                    that._postMessage();
                }
            }

            if (!!window.FileReader) {
                reader.readAsBinaryString(files.shp);
            } else {
                this.onFileLoad(reader.readAsBinaryString(o.shp));
            }

            if (files.dbf) {
                new DBF(files.dbf, function (data) {
                    that.addDBFDataToGeoJSON(data)
                    that._postMessage()
                });
            }
        },
        onFileLoad: function (data) {
            this.stream = new Gordon.Stream(data)

            this.readFileHeader()
            this.readRecords()
            this.formatIntoGeoJson()
        },
        _postMessage: function () {
            var data = {
                header: this.header,
                records: this.records,
                dbf: this.dbf,
                geojson: this.geojson
            }
            this.callback(data)
        },
        readFileHeader: function () {
            var s = this.stream,
                header = this.header = {}

            // The main file header is fixed at 100 bytes in length
            if (s < 100) throw "Invalid Header Length"

            // File code (always hex value 0x0000270a)
            header.fileCode = s.readSI32(true)

            if (header.fileCode != parseInt(0x0000270a))
                throw "Invalid File Code"

            // Unused; five uint32
            s.offset += 4 * 5

            // File length (in 16-bit words, including the header)
            header.fileLength = s.readSI32(true) * 2

            header.version = s.readSI32()

            header.shapeType = SHAPE_TYPES[s.readSI32()]

            // Minimum bounding rectangle (MBR) of all shapes contained within the shapefile; four doubles in the following order: min X, min Y, max X, max Y
            this._readBounds(header)

            // Z axis range
            header.rangeZ = {
                min: s.readDouble(),
                max: s.readDouble()
            }

            // User defined measurement range
            header.rangeM = {
                min: s.readDouble(),
                max: s.readDouble()
            }

        },
        readRecords: function () {
            var s = this.stream,
                records = this.records = [],
                record

            do {
                record = {}

                // Record number (1-based)
                record.id = s.readSI32(true)

                if (record.id == 0) break //no more records

                // Record length (in 16-bit words)
                record.length = s.readSI32(true) * 2

                record.shapeType = SHAPE_TYPES[s.readSI32()]

                // Read specific shape
                this["_read" + record.shapeType](record);

                records.push(record);

            } while (true);

        },
        _readBounds: function (object) {
            var s = this.stream

            object.bounds = {
                left: s.readDouble(),
                bottom: s.readDouble(),
                right: s.readDouble(),
                top: s.readDouble()
            }

            return object
        },
        _readParts: function (record) {
            var s = this.stream,
                nparts,
                parts = []

            nparts = record.numParts = s.readSI32()

            // since number of points always proceeds number of parts, capture it now
            record.numPoints = s.readSI32()

            // parts array indicates at which index the next part starts at
            while (nparts--) parts.push(s.readSI32())

            record.parts = parts

            return record
        },
        _readPoint: function (record) {
            var s = this.stream

            record.x = s.readDouble()
            record.y = s.readDouble()

            return record
        },
        _readPoints: function (record) {
            var s = this.stream,
                points = [],
                npoints = record.numPoints || (record.numPoints = s.readSI32())

            while (npoints--)
                points.push({
                    x: s.readDouble(),
                    y: s.readDouble()
                })

            record.points = points

            return record
        },
        _readMultiPoint: function (record) {
            var s = this.stream

            this._readBounds(record)
            this._readPoints(record)

            return record
        },
        _readPolygon: function (record) {
            var s = this.stream

            this._readBounds(record)
            this._readParts(record)
            this._readPoints(record)

            return record
        },
        _readPolyLine: function (record) {
            return this._readPolygon(record);
        },
        formatIntoGeoJson: function () {
            var bounds = this.header.bounds,
                records = this.records,
                features = [],
                feature, geometry, points, fbounds, gcoords, parts, point,
                geojson = {}

            geojson.type = "FeatureCollection"
            geojson.bbox = [
                bounds.left,
                bounds.bottom,
                bounds.right,
                bounds.top
            ]
            geojson.features = features

            for (var r = 0, record; record = records[r]; r++) {
                feature = {}, fbounds = record.bounds, points = record.points, parts = record.parts
                feature.type = "Feature"
                if (record.shapeType !== 'Point') {
                    feature.bbox = [
                        fbounds.left,
                        fbounds.bottom,
                        fbounds.right,
                        fbounds.top
                    ]
                }
                geometry = feature.geometry = {}

                switch (record.shapeType) {
                    case "Point":
                        geometry.type = "Point"
                        geometry.coordinates = [
                            record.x,
                            record.y]
                        break
                    case "MultiPoint":
                    case "PolyLine":
                        geometry.type = (record.shapeType == "PolyLine" ? "LineString" : "MultiPoint")
                        gcoords = geometry.coordinates = []

                        for (var p = 0; p < points.length; p++) {
                            var point = points[p]
                            gcoords.push([point.x, point.y])
                        }
                        break
                    case "Polygon":
                        geometry.type = "Polygon"
                        gcoords = geometry.coordinates = []

                        for (var pt = 0; pt < parts.length; pt++) {
                            var partIndex = parts[pt],
                                part = [],
                                point

                            // partIndex 0 == main poly, partIndex > 0 == holes in poly
                            for (var p = partIndex; p < (parts[pt + 1] || points.length); p++) {
                                point = points[p]
                                part.push([point.x, point.y])
                            }
                            gcoords.push(part)
                        }
                        break
                    default:
                }
                features.push(feature)
            }
            this.geojson = geojson

            if (this._addDataAfterLoad) this.addDBFDataToGeoJSON(this._addDataAfterLoad);
        },
        addDBFDataToGeoJSON: function (dbfData) {
            if (!this.geojson) return (this._addDataAfterLoad = dbfData)

            this.dbf = dbfData

            var features = this.geojson.features,
                len = features.length,
                records = dbfData.records

            while (len--) features[len].properties = records[len]
        }
    }

    window["Shapefile"] = Shapefile;
})(self);

