#!/usr/bin/env python
'''
Copyright (C) 2011- Swedish Meteorological and Hydrological Institute (SMHI)

This file is part of the bRopo extension to RAVE.

RAVE is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RAVE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with RAVE.  If not, see <http://www.gnu.org/licenses/>.
'''
## FMI's ROPO Anomaly Detection and Removal ...
## ... as a BALTRAD binary tool.

## @file
## @author Daniel Michelson, SMHI
## @date 2011-09-05

import _raveio
import _fmiimage
import _ropogenerator
import odim_source
from Proj import rd
import numpy
import _polarscan
import _polarscanparam
from rave_defines import UTF8


## Copies the top-level 'what' attributes from one object to another.
# Fixes /what/source in the process, if it doesn't contain the NOD identifier.
# Assumes NOD can be looked up based on WMO.
# @param ino input PVOL or SCAN object
# @param outo output PVOL or SCAN object
def copy_topwhat(ino, outo):
    outo.beamwidth = ino.beamwidth
    outo.date = ino.date
    outo.time = ino.time
    outo.height = ino.height
    outo.latitude = ino.latitude
    outo.longitude = ino.longitude

    S = odim_source.ODIM_Source(ino.source)
    if not S.nod:
        S.nod = odim_source.NOD[S.wmo]
        outo.source = odim_source.SOURCE[S.nod].encode(UTF8)
    else:
        outo.source = ino.source


## TODO: activate parameters list. This first version uses only the default DBZH.
#  TODO: separation of probability fields.
# @param scan input SCAN object
# @param options variable-length object containing argument names and values
# @returns scan object containing detected and removed anomalies
def process_scan(scan, options):
    if scan.elangle*rd < options.elev:
        image = _fmiimage.fromRave(scan, options.params)
        param = scan.getParameter(options.params)
        raw_thresh = int((int(options.threshold)-param.offset) / param.gain)
        rg = _ropogenerator.new(image).threshold(raw_thresh)

        if options.sepprob: detectors = 0
    
        if options.speck:
            a,b = eval(options.speck)
            rg.speck(a,b)
            if options.sepprob: detectors += 1

        if options.speckNormOld:
            a,b,c = eval(options.speckNormOld)
            rg.speckNormOld(a,b,c)
            if options.sepprob: detectors += 1

        if options.clutter:
            a,b = eval(options.clutter)
            rg.clutter(a,b)
            if options.sepprob: detectors += 1

        if options.clutter2:
            a,b = eval(options.clutter2)
            rg.clutter2(a,b)
            if options.sepprob: detectors += 1

        if options.softcut:
            a,b,c = eval(options.softcut)
            rg.softcut(a,b,c)
            if options.sepprob: detectors += 1

        if options.biomet:
            a,b,c,d = eval(options.biomet)
            rg.biomet(a,b,c,d)
            if options.sepprob: detectors += 1

        if options.ship:
            a,b = eval(options.ship)
            rg.ship(a,b)
            if options.sepprob: detectors += 1

        if options.emitter:
            a,b = eval(options.emitter)
            rg.emitter(a,b)
            if options.sepprob: detectors += 1
                
        if options.emitter2:
            a,b,c = eval(options.emitter2)
            rg.emitter2(a,b,c)
            if options.sepprob: detectors += 1
                
        if options.sun:
            a,b,c = eval(options.sun)
            rg.sun(a,b,c)
            if options.sepprob: detectors += 1

        if options.sun2:
            a,b,c,d,e = eval(options.sun2)
            rg.sun2(a,b,c,d,e)
            if options.sepprob: detectors += 1

        classification = rg.classify().classification.toRaveField()
        if options.restore:
            restored = rg.restore(int(options.restore_thresh)).toPolarScan()
        elif options.restore2:
            restored = rg.restore2(int(options.restore_thresh)).toPolarScan()
        restored.addQualityField(classification)

        if options.sepprob:
            for d in range(detectors):
                restored.addQualityField(rg.getProbabilityField(d).toRaveField())
        
        # Copy other parameter datasets in input scan
        for p in scan.getParameterNames():
            if not restored.hasParameter(p):
                restored.addParameter(scan.getParameter(p))
        # Copy also 'how' attributes at top of scan, if any
        for a in scan.getAttributeNames():
            restored.addAttribute(a, scan.getAttribute(a))
        # Do we need to copy any mandatory attributes at top of scan? 
        
        return restored
    else:
        return scan


## Loops through a volume and processes scans using \ref process_scan
# @param pvol input PVOL object
# @param options variable-length object containing argument names and values
# @returns PVOL object containing detected and removed anomalies
def process_pvol(pvol, options):
    import _polarvolume

    out = _polarvolume.new()
    copy_topwhat(pvol, out)

    for a in pvol.getAttributeNames():  # Copy also 'how' attributes at top level of volume, if any
        out.addAttribute(a, pvol.getAttribute(a))

    for s in range(pvol.getNumberOfScans()):
        scan = pvol.getScan(s)
        scan = process_scan(scan, options)
        out.addScan(scan)

    return out


if __name__ == "__main__":
    import sys
    from optparse import OptionParser

    usage = "usage: %prog -i <infile> --detector=args [--detector=args] ... [oh]"
    usage += "\n\nROPO was written by Markus Peura at FMI. This help text is based on information"
    usage += "\navailable in ROPO's code base."
    usage += "\nThis command-line tool has been built for the BALTRAD toolbox."
    usage += "\n\nIf no output file is given, the input file will be over-written."
    parser = OptionParser(usage=usage)

    parser.add_option("-i", "--input", dest="infile",
                      help="Name of input file to read. Can be either a polar scan or volume.")

    parser.add_option("-o", "--output", dest="outfile",
                      help="Name of output file to write. If not given, input file will be over-written.")

    parser.add_option("--parameters", dest="params",
                      default="DBZH",
                      help="Which radar parameter to process. Defaults to DBZH, but several can be comma-separated.")

    parser.add_option("--highest-elev", dest="elev",
                      default=2.0,
                      help="Data from elevation angles under this value will be processed. Defaults to 2.0 degrees.")

    parser.add_option("--separate-probabilities", dest="sepprob",
                      default=False,
                      help="True or False. Denotes whether to accumulate all detectors' probability fields into a maximum field, or keep them separate. Defaults to False.")

    parser.add_option("--threshold", dest="threshold",
                      default=-24,
                      help="dBZ threshold prior to processing. Defaults to -24 dBZ.")

    parser.add_option("--restore-thresh", dest="restore_thresh",
                      default=108,
                      help="Threshold raw value when applying probability classification to remove anomalies. Defaults to 108.")

    parser.add_option("--restore", dest="restore",  
                      default=True,
                      help="Uses the anomaly probability field to delete anomalies without filling holes. Defaults to True.")  

    parser.add_option("--restore-fill", dest="restore2",  
                      default=False,
                      help="Uses the anomaly probability field to delete anomalies AND fill in holes. Defaults to False.")  

    parser.add_option("--biomet", dest="biomet",
                      help="<dbz_max> <dbz_delta> <alt_max in meters> <alt_delta in km> Remove insect band e.g. -10,5,5000,1")

    parser.add_option("--clutter", dest="clutter",
                      help="<MIN_DBZ> <max_incomp> Remove specks under incompactness A, e.g. -5,5")

    parser.add_option("--clutter2", dest="clutter2",
                      help="<MIN_DBZ> <max_smooth> Remove specks under smoothness, e.g. -5,60")

    parser.add_option("--emitter", dest="emitter",
                      help="<MIN_DBZ> <LENGTH> Filter unity-width emitter lines, e.g. 10,4")

    parser.add_option("--emitter2", dest="emitter2",
                      help="<MIN_DBZ> <LENGTH in bins> <width in degrees> Filter emitter lines\ne.g. -10,3,3.")

    parser.add_option("--ship", dest="ship",
                      help="<min rel DBZ> <min A> Remove ships, e.g. 50,20")

    parser.add_option("--softcut", dest="softcut",
                      help="<max_dbz> <r in km> <r2 in km> Remove insect band, e.g. -10,250,100")

    parser.add_option("--speck", dest="speck",
                      help="<MIN_DBZ> <max_a in pixels> Threshold by min dBz, detect specks < A, e.g. -20,5")

    parser.add_option("--speckNormOld", dest="speckNormOld",
                      help="<MIN_DBZ> <max_a> <max_n> Threshold by min dBz, then detect specks, size A_max_range <=> size N*A A, e.g. -20,5,16")

    parser.add_option("--sun", dest="sun",
                      help="<MIN_DBZ>  <min_length> <max_thickness> Remove sun, e.g. -20,100,3")

    parser.add_option("--sun2", dest="sun2",
                      help="<MIN_DBZ>  <min_length> <max_thickness> <azimuth in degrees> <elevation in degrees> Remove sun, e.g. -20,100,3,45,2")

    parser.add_option("--lookup", dest="lookup", 
                      default=False,
                      help="Looks up which detectors to run, along with their arguments, from XML file. Useful for automated processing or just convenience. The NOD identifier of the /what/source attribute is used as the look-up key. If NOD doesn't exist, WMO is used to look up NOD. If no look-up exists for that radar, a default set of arguments is used. Using --lookup=True overrides all other detector-specific arguments.")

    (options, args) = parser.parse_args()

    if options.infile != None:  # More checks should be added.
        
        rio = _raveio.open(options.infile)

        if rio.objectType not in (_raveio.Rave_ObjectType_PVOL, _raveio.Rave_ObjectType_SCAN):
            print "Input file must be either polar scan or volume. Exiting ..."
            sys.exit(1)

        if options.lookup:
            import ropo_realtime
            ret = ropo_realtime.generate(rio.object)

        elif rio.objectType == _raveio.Rave_ObjectType_PVOL:
            ret = process_pvol(rio.object, options)

        elif rio.objectType == _raveio.Rave_ObjectType_SCAN:
            ret = process_scan(rio.object, options)
            copy_topwhat(rio.object, ret)
            
        output = _raveio.new()
        output.object = ret
        if options.outfile:
            output.filename = options.outfile
        else:
            output.filename = options.infile
        output.save()

    else:
        parser.print_help()
        sys.exit(1)
