/******************************************************************************
 * Copyright (c) 2004, Eric G. Miller
 *
 * This code is based in part on the earlier work of Frank Warmerdam
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ******************************************************************************
 * shpsort
 *
 * Rewrite a shapefile sorted by a field or by the geometry.  For polygons,
 * sort by area, for lines sort by length and do nothing for all others.
 */

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "shapefil.h"

enum FieldOrderEnum
{
    DESCENDING,
    ASCENDING
};
enum FieldTypeEnum
{
    FIDType = -2,
    SHPType = -1,
    StringType = FTString,
    LogicalType = FTLogical,
    IntegerType = FTInteger,
    DoubleType = FTDouble
};

struct DataUnion
{
    int null;
    union
    {
        int i;
        double d;
        char *s;
    } u;
};

struct DataStruct
{
    int record;
    struct DataUnion *value;
};

/*
   globals used in sorting, each element could have a pointer to
   a single data struct, but that's still nShapes pointers more
   memory.  Alternatively, write a custom sort rather than using
   library qsort.
*/
int nFields;
int *fldIdx;
int *fldOrder;
int *fldType;
int shpType;
int nShapes;

// TODO(schwehr): Use strdup.
static char *dupstr(const char *src)
{
    char *dst = malloc(strlen(src) + 1);
    if (!dst)
    {
        fprintf(stderr, "%s:%d: malloc failed!\n", __FILE__, __LINE__);
        exit(EXIT_FAILURE);
    }
    char *cptr = dst;
    while ((*cptr++ = *src++))
        ;
    return dst;
}

static char **split(const char *arg, const char *delim)
{
    char *copy = dupstr(arg);
    char **result = NULL;
    int i = 0;

    for (char *cptr = strtok(copy, delim); cptr; cptr = strtok(NULL, delim))
    {
        char **tmp = realloc(result, sizeof *result * (i + 1));
        if (!tmp && result)
        {
            while (i > 0)
            {
                free(result[--i]);
            }
            free(result);
            free(copy);
            return NULL;
        }
        result = tmp;
        result[i++] = dupstr(cptr);
    }

    free(copy);

    if (i)
    {
        char **tmp = realloc(result, sizeof *result * (i + 1));
        if (!tmp)
        {
            while (i > 0)
            {
                free(result[--i]);
            }
            free(result);
            return NULL;
        }
        result = tmp;
        result[i++] = NULL;
    }

    return result;
}

static void copy_related(const char *inName, const char *outName,
                         const char *old_ext, const char *new_ext)
{
    size_t name_len = strlen(inName);
    const size_t old_len = strlen(old_ext);
    const size_t new_len = strlen(new_ext);

    if (name_len < old_len)
        return;

    char *in = malloc(name_len - old_len + new_len + 1);
    memcpy(in, inName, name_len - old_len);
    strcpy(&in[(name_len - old_len)], new_ext);
    FILE *inFile = fopen(in, "rb");
    if (!inFile)
    {
        free(in);
        return;
    }
    name_len = strlen(outName);
    char *out = malloc(name_len - old_len + new_len + 1);
    if (!out)
    {
        fprintf(stderr, "%s:%d: couldn't copy related file!\n", __FILE__,
                __LINE__);
        fclose(inFile);
        free(in);
        free(out);
        return;
    }
    memcpy(out, outName, (name_len - old_len));
    strcpy(&out[(name_len - old_len)], new_ext);

    FILE *outFile = fopen(out, "wb");

    int c;
    while ((c = fgetc(inFile)) != EOF)
    {
        fputc(c, outFile);
    }
    fclose(inFile);
    fclose(outFile);
    free(in);
    free(out);
}

#ifdef DEBUG
static void PrintDataStruct(const struct DataStruct *data)
{
    for (int i = 0; i < nShapes; i++)
    {
        printf("data[%d] {\n", i);
        printf("\t.record = %d\n", data[i].record);
        for (int j = 0; j < nFields; j++)
        {
            printf("\t.value[%d].null = %d\n", j, data[i].value[j].null);
            if (!data[i].value[j].null)
            {
                switch (fldType[j])
                {
                    case FIDType:
                    case IntegerType:
                    case LogicalType:
                        printf("\t.value[%d].u.i = %d\n", j,
                               data[i].value[j].u.i);
                        break;
                    case DoubleType:
                    case SHPType:
                        printf("\t.value[%d].u.d = %f\n", j,
                               data[i].value[j].u.d);
                        break;
                    case StringType:
                        printf("\t.value[%d].u.s = %s\n", j,
                               data[i].value[j].u.s);
                        break;
                }
            }
        }
        puts("}");
    }
}
#endif

static double length2d_polyline(int n, const double *x, const double *y)
{
    double length = 0.0;
    for (int i = 1; i < n; i++)
    {
        length += sqrt((x[i] - x[i - 1]) * (x[i] - x[i - 1]) +
                       (y[i] - y[i - 1]) * (y[i] - y[i - 1]));
    }
    return length;
}

static double shp_length(SHPObject *feat)
{
    double length = 0.0;
    if (feat->nParts == 0)
    {
        length = length2d_polyline(feat->nVertices, feat->padfX, feat->padfY);
    }
    else
    {
        for (int part = 0; part < feat->nParts; part++)
        {
            int n;
            if (part < feat->nParts - 1)
            {
                n = feat->panPartStart[part + 1] - feat->panPartStart[part];
            }
            else
            {
                n = feat->nVertices - feat->panPartStart[part];
            }
            length +=
                length2d_polyline(n, &(feat->padfX[feat->panPartStart[part]]),
                                  &(feat->padfY[feat->panPartStart[part]]));
        }
    }
    return length;
}

static double area2d_polygon(int n, const double *x, const double *y)
{
    double area = 0;
    for (int i = 1; i < n; i++)
    {
        area += (x[i - 1] + x[i]) * (y[i] - y[i - 1]);
    }
    return area / 2.0;
}

static double shp_area(SHPObject *feat)
{
    double area = 0.0;
    if (feat->nParts == 0)
    {
        area = area2d_polygon(feat->nVertices, feat->padfX, feat->padfY);
    }
    else
    {
        for (int part = 0; part < feat->nParts; part++)
        {
            int n;
            if (part < feat->nParts - 1)
            {
                n = feat->panPartStart[part + 1] - feat->panPartStart[part];
            }
            else
            {
                n = feat->nVertices - feat->panPartStart[part];
            }
            area += area2d_polygon(n, &(feat->padfX[feat->panPartStart[part]]),
                                   &(feat->padfY[feat->panPartStart[part]]));
        }
    }
    /* our area function computes in opposite direction */
    return -area;
}

static int compare(const void *A, const void *B)
{
    const struct DataStruct *a = A;
    const struct DataStruct *b = B;

    for (int i = 0; i < nFields; i++)
    {
        if (a->value[i].null && b->value[i].null)
        {
            continue;
        }
        if (a->value[i].null && !b->value[i].null)
        {
            return (fldOrder[i]) ? 1 : -1;
        }
        if (!a->value[i].null && b->value[i].null)
        {
            return (fldOrder[i]) ? -1 : 1;
        }
        switch (fldType[i])
        {
            case FIDType:
            case IntegerType:
            case LogicalType:
                if (a->value[i].u.i < b->value[i].u.i)
                {
                    return (fldOrder[i]) ? -1 : 1;
                }
                if (a->value[i].u.i > b->value[i].u.i)
                {
                    return (fldOrder[i]) ? 1 : -1;
                }
                break;
            case DoubleType:
            case SHPType:
                if (a->value[i].u.d < b->value[i].u.d)
                {
                    return (fldOrder[i]) ? -1 : 1;
                }
                if (a->value[i].u.d > b->value[i].u.d)
                {
                    return (fldOrder[i]) ? 1 : -1;
                }
                break;
            case StringType:
            {
                const int result = strcmp(a->value[i].u.s, b->value[i].u.s);
                if (result)
                {
                    return (fldOrder[i]) ? result : -result;
                }
                break;
            }
            default:
                fprintf(stderr,
                        "compare: Program Error! Unhandled field type! "
                        "fldType[%d] = %d\n",
                        i, fldType[i]);
                break;
        }
    }
    return 0;
}

static struct DataStruct *build_index(SHPHandle shp, DBFHandle dbf)
{
    struct DataStruct *data = malloc(sizeof *data * nShapes);
    if (!data)
    {
        fputs("malloc failed!\n", stderr);
        exit(EXIT_FAILURE);
    }

    /* populate array */
    for (int i = 0; i < nShapes; i++)
    {
        data[i].value = malloc(sizeof data[0].value[0] * nFields);
        if (0 == data[i].value)
        {
            fputs("malloc failed!\n", stderr);
            exit(EXIT_FAILURE);
        }
        data[i].record = i;
        for (int j = 0; j < nFields; j++)
        {
            data[i].value[j].null = 0;
            switch (fldType[j])
            {
                case FIDType:
                    data[i].value[j].u.i = i;
                    break;
                case SHPType:
                {
                    SHPObject *feat = SHPReadObject(shp, i);
                    switch (feat->nSHPType)
                    {
                        case SHPT_NULL:
                            fprintf(stderr, "Shape %d is a null feature!\n", i);
                            data[i].value[j].null = 1;
                            break;
                        case SHPT_POINT:
                        case SHPT_POINTZ:
                        case SHPT_POINTM:
                        case SHPT_MULTIPOINT:
                        case SHPT_MULTIPOINTZ:
                        case SHPT_MULTIPOINTM:
                        case SHPT_MULTIPATCH:
                            /* Y-sort bounds */
                            data[i].value[j].u.d = feat->dfYMax;
                            break;
                        case SHPT_ARC:
                        case SHPT_ARCZ:
                        case SHPT_ARCM:
                            data[i].value[j].u.d = shp_length(feat);
                            break;
                        case SHPT_POLYGON:
                        case SHPT_POLYGONZ:
                        case SHPT_POLYGONM:
                            data[i].value[j].u.d = shp_area(feat);
                            break;
                        default:
                            fputs("Can't sort on Shapefile feature type!\n",
                                  stderr);
                            exit(EXIT_FAILURE);
                    }
                    SHPDestroyObject(feat);
                    break;
                }
                case FTString:
                    data[i].value[j].null =
                        DBFIsAttributeNULL(dbf, i, fldIdx[j]);
                    if (!data[i].value[j].null)
                    {
                        data[i].value[j].u.s =
                            dupstr(DBFReadStringAttribute(dbf, i, fldIdx[j]));
                    }
                    break;
                case FTInteger:
                case FTLogical:
                    data[i].value[j].null =
                        DBFIsAttributeNULL(dbf, i, fldIdx[j]);
                    if (!data[i].value[j].null)
                    {
                        data[i].value[j].u.i =
                            DBFReadIntegerAttribute(dbf, i, fldIdx[j]);
                    }
                    break;
                case FTDouble:
                    data[i].value[j].null =
                        DBFIsAttributeNULL(dbf, i, fldIdx[j]);
                    if (!data[i].value[j].null)
                    {
                        data[i].value[j].u.d =
                            DBFReadDoubleAttribute(dbf, i, fldIdx[j]);
                    }
                    break;
            }
        }
    }

#ifdef DEBUG
    PrintDataStruct(data);
    fputs("build_index: sorting array\n", stdout);
#endif

    qsort(data, nShapes, sizeof data[0], compare);

#ifdef DEBUG
    PrintDataStruct(data);
    fputs("build_index: returning array\n", stdout);
#endif

    return data;
}

int main(int argc, char *argv[])
{
    if (argc < 4)
    {
        printf("USAGE: shpsort <infile> <outfile> <field[;...]> "
               "[<(ASCENDING|DESCENDING)[;...]>]\n");
        exit(EXIT_FAILURE);
    }

    SHPHandle inSHP = SHPOpen(argv[1], "rb");
    if (!inSHP)
    {
        fputs("Couldn't open shapefile for reading!\n", stderr);
        exit(EXIT_FAILURE);
    }
    SHPGetInfo(inSHP, &nShapes, &shpType, NULL, NULL);

    /* If we can open the inSHP, open its DBF */
    DBFHandle inDBF = DBFOpen(argv[1], "rb");
    if (!inDBF)
    {
        fputs("Couldn't open dbf file for reading!\n", stderr);
        exit(EXIT_FAILURE);
    }

    /* Parse fields and validate existence */
    char **fieldNames = split(argv[3], ";");
    if (!fieldNames)
    {
        fputs("ERROR: parsing field names!\n", stderr);
        exit(EXIT_FAILURE);
    }

    fldIdx = malloc(sizeof *fldIdx * nFields);
    if (!fldIdx)
    {
        fputs("malloc failed!\n", stderr);
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < nFields; i++)
    {
        int len = (int)strlen(fieldNames[i]);
        while (len > 0)
        {
            --len;
            fieldNames[i][len] =
                (char)toupper((unsigned char)fieldNames[i][len]);
        }
        fldIdx[i] = DBFGetFieldIndex(inDBF, fieldNames[i]);
        if (fldIdx[i] < 0)
        {
            /* try "SHAPE" */
            if (strcmp(fieldNames[i], "SHAPE") == 0)
            {
                fldIdx[i] = -1;
            }
            else if (strcmp(fieldNames[i], "FID") == 0)
            {
                fldIdx[i] = -2;
            }
            else
            {
                fprintf(stderr, "ERROR: field '%s' not found!\n",
                        fieldNames[i]);
                exit(EXIT_FAILURE);
            }
        }
    }

    /* set up field type array */
    fldType = malloc(sizeof *fldType * nFields);
    if (!fldType)
    {
        fputs("malloc failed!\n", stderr);
        exit(EXIT_FAILURE);
    }
    int width;
    int decimals;
    for (int i = 0; i < nFields; i++)
    {
        if (fldIdx[i] < 0)
        {
            fldType[i] = fldIdx[i];
        }
        else
        {
            fldType[i] =
                DBFGetFieldInfo(inDBF, fldIdx[i], NULL, &width, &decimals);
            if (fldType[i] == FTInvalid)
            {
                fputs("Unrecognized field type in dBASE file!\n", stderr);
                exit(EXIT_FAILURE);
            }
        }
    }

    /* set up field order array */
    fldOrder = malloc(sizeof *fldOrder * nFields);
    if (!fldOrder)
    {
        fputs("malloc failed!\n", stderr);
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < nFields; i++)
    {
        /* default to ascending order */
        fldOrder[i] = ASCENDING;
    }
    if (argc > 4)
    {
        char **strOrder = split(argv[4], ";");
        if (!strOrder)
        {
            fputs("ERROR: parsing fields ordering!\n", stderr);
            exit(EXIT_FAILURE);
        }
        for (int i = 0; i < nFields && strOrder[i]; i++)
        {
            if (strcmp(strOrder[i], "DESCENDING") == 0)
            {
                fldOrder[i] = DESCENDING;
            }
        }
    }

    /* build the index */
    struct DataStruct *index = build_index(inSHP, inDBF);

    /* Create output shapefile */
    SHPHandle outSHP = SHPCreate(argv[2], shpType);
    if (!outSHP)
    {
        fprintf(stderr, "%s:%d: couldn't create output shapefile!\n", __FILE__,
                __LINE__);
        exit(EXIT_FAILURE);
    }

    /* Create output dbf */
    DBFHandle outDBF = DBFCloneEmpty(inDBF, argv[2]);
    if (!outDBF)
    {
        fprintf(stderr, "%s:%d: couldn't create output dBASE file!\n", __FILE__,
                __LINE__);
        exit(EXIT_FAILURE);
    }

    /* Copy projection file, if any */
    copy_related(argv[1], argv[2], ".shp", ".prj");

    /* Copy metadata file, if any */
    copy_related(argv[1], argv[2], ".shp", ".shp.xml");

    /* Write out sorted results */
    for (int i = 0; i < nShapes; i++)
    {
        SHPObject *feat = SHPReadObject(inSHP, index[i].record);
        if (SHPWriteObject(outSHP, -1, feat) < 0)
        {
            fprintf(stderr, "%s:%d: error writing shapefile!\n", __FILE__,
                    __LINE__);
            exit(EXIT_FAILURE);
        }
        void *tuple = (void *)DBFReadTuple(inDBF, index[i].record);
        if (DBFWriteTuple(outDBF, i, tuple) < 0)
        {
            fprintf(stderr, "%s:%d: error writing dBASE file!\n", __FILE__,
                    __LINE__);
            exit(EXIT_FAILURE);
        }
    }
    SHPClose(inSHP);
    SHPClose(outSHP);
    DBFClose(inDBF);
    DBFClose(outDBF);

    return EXIT_SUCCESS;
}
