# Copyright 2017 Planet Labs, Inc.
#
# Licensed 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.

import json
import re

import click
from click.types import CompositeParamType

from .util import read
from .util import _split

from planet.api import filters
from planet.api.utils import geometry_from_json
from planet.api.utils import strp_lenient
from planet.scripts.item_asset_types import get_item_types, get_asset_types, \
    get_bundles, DEFAULT_ITEM_TYPES, DEFAULT_ASSET_TYPES, DEFAULT_BUNDLES


metavar_docs = {
    'FIELD COMP VALUE...': '''A comparison query format where FIELD is a
    property of the item-type and COMP is one of lt, lte, gt, gte and VALUE is
    the number or date to compare against.

    Note: ISO-8601 variants are supported. For example, ``2017`` is short for
    ``2017-01-01T00:00:00+00:00``.
    ''',
    'FIELD VALUES...': '''Specifies an 'in' query where FIELD is a property
    of the item-type and VALUES is space or comma separated text or numbers.
    ''',
    'GEOM': '''Specify a geometry in GeoJSON format either as an inline value,
    stdin, or a file. ``@-`` specifies stdin and ``@filename`` specifies
    reading from a file named 'filename'. Otherwise, the value is assumed to
    be GeoJSON.
    ''',
    'FILTER': '''Specify a Data API search filter provided as JSON.
    ``@-`` specifies stdin and ``@filename`` specifies reading from a file
    named 'filename'. Otherwise, the value is assumed to be JSON.
    ''',
    'ITEM-TYPE': '''Specify Item-Type(s) of interest. Case-insensitive,
    supports glob-matching, e.g. ``psscene*`` means ``PSScene``,
    ``PSScene3Band``, and ``PSScene4Band``. The ``all`` value specifies
    every Item-Type.
    ''',
    'ASSET-TYPE': '''Specify Asset-Type(s) of interest. Case-insenstive,
    supports glob-matching, e.g. ``visual*`` specifies ``visual`` and
    ``visual_xml``.
    '''
}


class _LenientChoice(click.Choice):
    '''Like click.Choice but allows
    case-insensitive prefix matching
    optional 'all' matching
    optional prefix matching
    glob matching
    format fail msges for large selection of choices

    returns a list unlike choice (to support 'all')
    '''

    allow_all = False
    allow_prefix = False

    def get_metavar(self, param):
        return self.name.upper()

    def _fail(self, msg, val, param, ctx):
        self.fail('%s choice: %s.\nChoose from:\n\t%s' %
                  (msg, val, '\n\t'.join(self.choices)), param, ctx)

    def convert(self, val, param, ctx):
        matched = set()
        for p in _split(val):
            matched = matched.union(self._match(p, param, ctx))
        return list(matched)

    def _match(self, val, param, ctx, update_on_fail=True):
        lval = val.lower()
        if lval == 'all' and self.allow_all:
            return self.choices
        if '*' in lval:
            pat = lval.replace('*', '.*')
            matches = [c for c in self.choices
                       if re.match(pat, c.lower())]
        elif self.allow_prefix:
            matches = [c for c in self.choices
                       if c.lower().startswith(lval)]
        else:
            matches = [c for c in self.choices if c.lower() == lval]
        if not matches and update_on_fail:
            self._update_choices()  # get choices from a remote source.
            return self._match(val, param, ctx, False)  # Try one more time
        elif not matches:
            self._fail('invalid', val, param, ctx)
        else:
            return matches

    def _update_choices(self):
        self.choices = self.get_remote_choices()

    def get_remote_choices(self):
        '''
        No-op just returns current choices.
        Subclasses should override.
        '''
        return self.choices


class Bundle(_LenientChoice):
    name = 'bundle'
    allow_all = True

    def __init__(self):
        _LenientChoice.__init__(self, DEFAULT_BUNDLES)

    def get_remote_choices(self):
        return get_bundles()


class ItemType(_LenientChoice):
    name = 'item-type'
    allow_all = True
    allow_prefix = False

    def __init__(self):
        _LenientChoice.__init__(self, DEFAULT_ITEM_TYPES)

    def get_remote_choices(self):
        return get_item_types()


class AssetType(_LenientChoice):
    name = 'asset-type'

    def __init__(self):
        _LenientChoice.__init__(self, DEFAULT_ASSET_TYPES)

    def get_remote_choices(self):
        return get_asset_types()


class AssetTypePerm(AssetType):

    @staticmethod
    def to_permissions(asset_types):
        return filters.permission_filter(*[
            'assets.%s:download' % a for a in asset_types
        ])

    def convert(self, val, param, ctx):
        return AssetTypePerm.to_permissions(
            AssetType.convert(self, val, param, ctx))


class _FilterFieldValues(CompositeParamType):
    name = 'field values'
    arity = 2

    def convert(self, val, param, ctx):
        field, vals = val
        vals = _split(vals)
        parsed = []
        for v in vals:
            v = v.strip()
            if not v:
                continue
            try:
                parsed.append(self.val_type(v))
            except ValueError:
                self.fail('invalid value: %s' % v, param, ctx)
        return self._builder(field, *parsed)


class StringIn(_FilterFieldValues):
    val_type = str

    @property
    def _builder(self):
        return filters.string_filter


class NumberIn(_FilterFieldValues):
    val_type = float

    @property
    def _builder(self):
        return filters.num_filter


class Range(CompositeParamType):
    arity = 3
    name = 'field comp value'
    comp_ops = ['lt', 'lte', 'gt', 'gte']

    @property
    def _builder(self):
        return filters.range_filter

    def _parse(self, val, param, ctx):
        try:
            return float(val)
        except ValueError:
            self.fail('invalid value for range: "%s", must be number' % val)

    def convert(self, vals, param, ctx):
        field, comp_op, val = vals
        if comp_op not in self.comp_ops:
            self.fail('invalid operator: %s. allowed: %s' % (
                comp_op, ','.join(self.comp_ops)), param, ctx)
        args = dict([(comp_op, self._parse(val, param, ctx))])
        return self._builder(field, **args)


class DateRange(Range):
    @property
    def _builder(self):
        return filters.date_range

    def _parse(self, val, param, ctx):
        parsed = strp_lenient(val)
        if parsed is None:
            self.fail('invalid date: %s.' % val, param, ctx)
        return parsed


class GeomFilter(click.ParamType):
    name = 'geom'

    def convert(self, val, param, ctx):
        val = read(val)
        if not val:
            return []
        try:
            geoj = json.loads(val)
        except ValueError:
            raise click.BadParameter('invalid GeoJSON')
        geom = geometry_from_json(geoj)
        if geom is None:
            raise click.BadParameter('unable to find geometry in input')
        return [filters.geom_filter(geom)]


class FilterJSON(click.ParamType):
    name = 'filter'

    def convert(self, val, param, ctx):
        val = read(val)
        if not val:
            return []
        try:
            filt = json.loads(val)
        except ValueError:
            raise click.BadParameter('invalid JSON')
        if not filters.is_filter_like(filt):
            self.fail('Does not appear to be valid filter', param, ctx)
        return filt


class SortSpec(CompositeParamType):
    name = 'field order'
    arity = 2

    def convert(self, val, param, ctx):
        if not val:
            return ''
        field, order = val
        fields = ('published', 'acquired')
        if field not in fields:
            raise click.BadParameter(
                'sort only supports: %s' % ' '.join(fields))
        orders = ('asc', 'desc')
        if order not in orders:
            raise click.BadParameter(
                'order only supports: %s' % ' '.join(orders))
        return ' '.join(val)


class BoundingBox(click.ParamType):
    name = 'bbox'

    def convert(self, val, param, ctx):
        try:
            xmin, ymin, xmax, ymax = map(float, val.split(','))
        except (TypeError, ValueError):
            raise click.BadParameter('Invalid bounding box')
        return (xmin, ymin, xmax, ymax)


class DateInterval(click.ParamType):
    name = 'date interval'

    def convert(self, val, param, ctx):
        dates = val.split('/')
        if len(dates) > 2:
            raise click.BadParameter('Too many dates')

        for date in dates:
            if date != '..' and strp_lenient(date) is None:
                raise click.BadParameter('Invalid date: {}'.format(date))


class ClipAOI(click.ParamType):
    name = 'clip'

    def convert(self, val, param, ctx):
        val = read(val)
        if not val:
            return []
        try:
            json.loads(val)
        except ValueError:
            raise click.BadParameter('invalid GeoJSON')
        return val


class RequiredUnless(click.Option):
    def __init__(self, *args, **kwargs):
        self.this_opt_exists = kwargs.pop('this_opt_exists')
        super(RequiredUnless, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        if self.name not in opts and self.this_opt_exists not in opts:
            raise click.UsageError('{} is required.'.format(self.name))
        return super(RequiredUnless, self).handle_parse_result(ctx, opts, args)
