#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# GenLabels : Generate Barcode Labels for Tape Cartridges
#
# (c) 2008-2014 Jerome Alet <alet@librelogiciel.com>
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# $Id: $
#

"""This is the generic client to MyTapeLabels.com's XML-RPC API to generate Barcode Labels for Tape Cartridges in the PDF format."""

import sys
import os
import socket
import optparse
import xmlrpclib

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

This program 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>."""

__version__ = "0.5"

DEFAULT_URL = "http://api.mytapelabels.com"

class TLOptionParser(optparse.OptionParser) :
    """This class to define additional methods, and different help formatting, from the traditional OptionParser."""
    def __init__(self, *args, **kwargs) :
        """Initializes our option parser with additional attributes."""
        self.examples = []
        kwargs["version"] = "%s (MyTapeLabels.com) v%s" \
            % (os.path.basename(sys.argv[0]),
               __version__)
        optparse.OptionParser.__init__(self, *args, **kwargs)
        self.disable_interspersed_args()
        self.remove_version_and_help()
        self.add_generic_options()

    def format_help(self, formatter=None) :
        """Reformats help our way : adding examples and copyright message at the end."""
        if formatter is None :
            formatter = self.formatter
        result = []
        result.append(optparse.OptionParser.format_help(self, formatter) + "\n")
        result.append(self.format_examples())
        result.append(self.format_copyright())
        return "".join(result)

    #
    # Specific additions
    #
    def format_examples(self, formatter=None) :
        """Formats examples our way."""
        if formatter is None :
            formatter = self.formatter
        result = []
        if self.examples :
            result.append(formatter.format_heading("examples"))
            formatter.indent()
            for (cmd, explanation) in self.examples :
                result.append(formatter.format_description(self.expand_prog_name(cmd)))
                result.append(formatter.format_description(self.expand_prog_name(explanation)) + "\n")
            formatter.dedent()
        return "".join(result)

    def format_copyright(self, formatter=None) :
        """Formats copyright message our way."""
        if formatter is None :
            formatter = self.formatter
        result = []
        result.append(formatter.format_heading("licensing terms"))
        formatter.indent()
        result.append(formatter.format_description("(c) 2008-2014 Jerome Alet - <alet@librelogiciel.com>\n"))
        for part in __gplblurb__.split("\n\n") :
            result.append(formatter.format_description(part) + "\n")
        formatter.dedent()
        return "".join(result)

    def add_example(self, command, doc) :
        """Adds an usage example."""
        self.examples.append(("%prog " + command, doc))

    def remove_version_and_help(self) :
        """Removes the default definitions for options version and help."""
        for o in ("-h", "-help", "--help", "-v", "-version", "--version") :
            try :
                self.remove_option(o)
            except ValueError :
                pass

    def add_generic_options(self) :
        """Adds common options."""
        self.add_option("-h", "--help",
                              action="help",
                              help="Show this help message and exit.")
        self.add_option("-v", "--version",
                              action="version",
                              help="Show the version number and exit.")

class GenLabels :
    """Client for Barcode Tape Labels generator."""
    def __init__(self, options, arguments) :
        """Initialization."""
        self.server = self.session = self.labels = None
        self.options = options
        if not options.apikey :
            self.error("The API key parameter is mandatory. See help. You can obtain an API key from http://api.mytapelabels.com")
        if not options.tapetype :
            self.error("The tape type parameter is mandatory. See help.")
        self.availabletemplates = \
            self.availablemediatypes = \
            self.availablestyles = \
            self.availablecolorschemes = None
        self.server = xmlrpclib.ServerProxy(options.url)
        try :
            self.session = self.server.login(options.apikey, options.tapetype)
        except socket.error, msg :
            self.error("MyTapeLabels' API server seems to be down, please retry later or contact team@mytapelabels.com : %s" % msg)
        except xmlrpclib.Fault, msg :
            self.error("Problem when connecting to MyTapeLabels' API server : %s" % msg)
        else :
            self.checkVersion()
            if not self.options.template :
                if self.options.style and arguments and (arguments != ["-"]) :
                    self.error("The template parameter is mandatory. See help.")
            else :
                self.checkTemplate(self.options.template)
                if not self.options.style :
                    self.error("The style parameter is mandatory. See help.")
                else :
                    self.checkStyle(self.options.style)
                if not arguments :
                    self.error("You must specify a list of labels to generate. See help.")
                self.checkMediaType(self.options.mediatype)
                self.checkColorScheme(self.options.colorscheme)
                if (arguments[0] == "-") and (self.options.apikey != "DEMO") :
                    self.labels = [l.strip() for l in sys.stdin.readlines()]
                else :
                    self.labels = arguments

    def __del__(self) :
        """Ensures the session gets properly closed."""
        if (self.server is not None) and self.session :
            self.server.logout(self.session)
            self.server = self.session = None

    def error(self, message) :
        """Outputs an error messages and exits with an error."""
        sys.stderr.write("%s\n" % message)
        sys.exit(-1)

    def formatAvailableList(self, title, available) :
        """Returns a formatted list of available things."""
        lines = [ title ]
        keys = available.keys()
        keys.sort()
        for key in keys :
            lines.append("  %s (for %s)" % (key, available[key]))
        return "\n".join(lines)

    def checkVersion(self) :
        """Ensures that the client's version is the same as the server's version, otherwise exits while urging to upgrade."""
        apiversion = self.server.getAPIVersion(self.session)
        if apiversion != __version__ :
            self.error("The client's version %s doesn't match the API's one %s. Please upgrade your client from http://api.mytapelabels.com/client/\n" % (__version__, apiversion))

    def checkTemplate(self, template) :
        """Checks if template name is valid. Exits with an error message if it is not valid."""
        if self.availabletemplates is None :
            self.availabletemplates = self.server.getAvailableTemplates(self.session)
        if not self.availabletemplates.has_key(template) :
            self.error(("Invalid template name %s.\n" % repr(template)) + self.listAvailableTemplates())

    def checkMediaType(self, mediatype) :
        """Checks if media type name is valid. Exits with an error message if it is not valid."""
        if mediatype :
            if self.availablemediatypes is None :
                self.availablemediatypes = self.server.getAvailableMediaTypes(self.session)
            if not self.availablemediatypes.has_key(mediatype) :
                self.error(("Invalid media type name %s.\n" % repr(mediatype)) + self.listAvailableMediaTypes())
        
    def checkStyle(self, style) :
        """Checks if style name is valid. Exits with an error message if it is not valid."""
        if self.availablestyles is None :
            self.availablestyles = self.server.getAvailableStyles(self.session)
        if not self.availablestyles.has_key(style) :
            self.error(("Invalid style name %s.\n" % repr(style)) + self.listAvailableStyles())

    def checkColorScheme(self, colorscheme) :
        """Checks if color scheme is valid. Exits with an error message if it is not valid."""
        if self.availablecolorschemes is None :
            self.availablecolorschemes = self.server.getAvailableColorSchemes(self.session)
        if not self.availablecolorschemes.has_key(colorscheme) :
            self.error(("Invalid color scheme %s.\n" % repr(colorscheme)) + self.listAvailableColorSchemes())
        
    def listAvailableTemplates(self) :
        """Returns a formatted list of available templates."""
        return self.formatAvailableList("Available templates :", self.availabletemplates)

    def listAvailableMediaTypes(self) :
        """Returns a formatted list of available media types."""
        return self.formatAvailableList("Available media types :", self.availablemediatypes)

    def listAvailableStyles(self) :
        """Returns a formatted list of available styles."""
        return self.formatAvailableList("Available styles :", self.availablestyles)

    def listAvailableColorSchemes(self) :
        """Returns a formatted list of available colorschemes."""
        return self.formatAvailableList("Available color schemes :", self.availablecolorschemes)

    def displayKeyInfo(self) :
        """Displays information about the API key currently in use."""
        (fullname, 
         email,
         apikey,
         maxlabels,
         validuntil,
         remaining) = self.server.getAPIKeyDetails(self.session)
        sys.stdout.write("API Key : %s\n" % apikey)
        sys.stdout.write("\t                   Owner : %s - <%s>\n" % (fullname, email))
        sys.stdout.write("\tNumber of labels allowed : %i\n" % maxlabels)
        sys.stdout.write("\t Labels generated so far : %i\n" % (maxlabels - remaining))
        sys.stdout.write("\t        Remaining labels : %i\n" % remaining)
        sys.stdout.write("\t         Expiration date : %s\n" % validuntil)

    def generatePDF(self) :
        """Generates the final PDF document, and if ok saves it to a file or stdout. Prints errors to stderr."""
        (pdfcontent, 
         errormessage) = self.server.generatePDF(self.session,
                                                 self.options.template,
                                                 self.options.mediatype,
                                                 self.options.style,
                                                 self.options.colorscheme,
                                                 self.options.borders,
                                                 self.options.checksum,
                                                 self.options.xoffset,
                                                 self.options.yoffset,
                                                 self.options.preview,
                                                 self.labels)
        if errormessage :
            sys.stderr.write("%s\n" % errormessage)
        if pdfcontent.data :
            if self.options.outputfile != "-" :
                fout = open(self.options.outputfile, "wb")
            else :
                if sys.platform.startswith("win") :
                    # Under MS-Windows, we must put stdout to binary mode
                    # otherwise the PDF documents would be broken.
                    import msvcrt
                    msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
                fout = sys.stdout
            fout.write(pdfcontent.data)
            if self.options.outputfile != "-" :
                fout.close()
            return True
        else :
            sys.stderr.write("No PDF Barcode Labels was generated. Please double check your command line arguments, and read the help.\n")
            return False

# 
# Main code
#
if __name__ == "__main__" :
    parser = TLOptionParser(description="Generates PDF documents containing tape barcode labels using MyTapeLabels.com's XML-RPC API.",
                             usage="genlabels {--apikey KEY} {--template TEMPLATE} [options] [label1 label2 ... labelN]")

    parser.add_option("-a", 
                      "--apikey",
                      dest="apikey",
                      default="DEMO",
                      help="Set the API key to use when connecting to MyTapeLabels' API. Defaults to '%default', which helps you freely calibrate your printing offsets with some demonstration labels. To produce real labels, you must purchase an API key from http://www.mytapelabels.com")

    parser.add_option("-b",
                      "--borders",
                      dest="borders",
                      action="store_true",
                      default=False,
                      help="Use this option if you want label borders to be drawn to ease manual cutting. This is usually not needed with proper blank labels templates and so it defaults to %default.") 

    parser.add_option("-c", 
                      "--colorscheme",
                      dest="colorscheme",
                      default="monochrome",
                      help="Set the color scheme to use. It defaults to '%default'.")

    parser.add_option("-C",
                      "--checksum",
                      dest="checksum",
                      action="store_true",
                      default=False,
                      help="Use this option if your tape library requires checksummed barcodes. This is rarely needed and so it defaults to %default.")

    parser.add_option("-m",
                      "--mediatype",
                      dest="mediatype",
                      default="",
                      help="Set the default mediatype to generate labels for. This value which defaults to '%default' is concatenated to all 6 characters labels. This option is not necessary if all your labels are 8 characters long.")

    parser.add_option("-p",
                      "--preview",
                      action="store_true",
                      default=False,
                      help="Generate a preview : all barcodes will read 'PREVIEW0' but all human readable text will be as requested. Labels generated this way are not meant to be printed, but only serve as a visual check before generating the real labels.")

    parser.add_option("-o",
                      "--outputfile",
                      dest="outputfile",
                      default="-",
                      help="Set the filename of the PDF document that will be generated. It defaults to '%default' which means standard output (stdout).")

    parser.add_option("-s",
                      "--style",
                      dest="style",
                      default="VerticalTop",
                      help="Set the label's style : position and orientation of the text wrt the barcode. It defaults to '%default'.")

    parser.add_option("-T",
                      "--tapetype",
                      dest="tapetype",
                      default="LTO",
                      help="Set the tape type to generate labels for. Supported values are 'LTO' and 'STK10K'. The default tape type is '%default'.")

    parser.add_option("-t",
                      "--template",
                      dest="template",
                      help="Set the blank labels template to use.")

    parser.add_option("-u",
                      "--url",
                      dest="url",
                      default=DEFAULT_URL,
                      help="Set the MyTapeLabels API URL to connect to. It defaults to '%default'")

    parser.add_option("-x",
                      "--xoffset",
                      dest="xoffset",
                      action="store",
                      type="float",
                      default=0.0,
                      help="Offset printing by this value millimeters to the right. Negative values are allowed. This allows you to adjust printing to your printer's precision. It defaults to %default mm.")

    parser.add_option("-y",
                      "--yoffset",
                      dest="yoffset",
                      action="store",
                      type="float",
                      default=0.0,
                      help="Offset printing by this value millimeters to the top. Negative values are allowed. This allows you to adjust printing to your printer's precision. It defaults to %default mm.")

    (options, arguments) = parser.parse_args()
    if not arguments :
        arguments.append("-") # Takes labels from stdin if there's none on the command line
    generator = GenLabels(options, arguments)
    if not options.template :
        generator.displayKeyInfo()
        sys.exit(0)
    elif generator.generatePDF() :
        sys.exit(0)
    else :
        sys.exit(-2)

