#! /usr/bin/env python
# -*- coding: utf-8 -*-

# -----------------------------------------
# PLASMA installer
# University of Tennessee Knoxville
# June 24, 2009
# ----------------------------------------

from utils import runShellCommand, writefile, killfiles, fixpaths
import sys
import os
import getopt
import string

class Framework:
    """ This class takes care of the PLASMA installation. """

    # set default values
    prefix = ""
    make         = "make"                    # the "make" command
    plat         = "LINUX"
    mangling     = ""                        # sets the mangling type for Fortran to C calls
    plasmalib = ""                           # plasma library
    proxy        = 0 
    downblas     = 0                         # whether or not to download reference blas
    testing      = 1                         # whether on not to compile and run PLASMA test programs
    downcmd      = ""                        # the command used to download stuff
    plasmaurl    = "http://icl.cs.utk.edu/projectsfiles/plasma/pubs/plasma.tar.gz"
    urlbase      = "http://icl.cs.utk.edu/projectsfiles/plasma/pubs"
    versions     = ("2.1.0", "2.0.0", "1.0.1", "1.0.0")
    ranlib       = ""                        # the ranlib command
    clean        = 0
    nbcores      = 2
    src_path     = ""

    def __init__(self, argv, config):

        print "="*40
        print "Setting up the framework\n"

        self.config = config

        # parse input arguments
        self.parse_args(argv)

        if self.clean:
            self.cleanup()
            sys.exit()

        if self.prefix:
            if(not os.path.isdir(os.path.join(os.getcwd(),self.prefix))):
               print"Creating directory",self.prefix
               os.mkdir(os.path.join(os.getcwd(),self.prefix))
            os.chdir(self.prefix)

        if self.config.cc == '':
            #  if no C compiler is provided
            #  raise an error
            print """
A C compiler is required to compile the Plasma library.
You can specify it using the --cc flag."""
            sys.exit()

        if self.config.fc == "":
            #  if no Fortran compiler is provided
            #  raise an error
            print """
A Fortran compiler is required to compile the core_lapack library.
You can specify it using the --fc flag."""
            sys.exit()

        # if testing has to be done, BLAS is required
        if self.testing:
            if self.config.blaslib == "" and self.downblas == 0:
                print """
Please provide a working BLAS library. If a BLAS library
is not present on the system, the reference BLAS library it can be
automatically downloaded and installed by adding the --downblas flag.
Be aware that a reference BLAS library will be installed with the --downblas
flag so don't expect good performance.
You can specify the optimized BLAS library **installed** on your machine using the --blaslib option
For instance 
    --blaslib="-lessl" for IBM BLAS, 
    --blaslib="-framework veclib" for Mac OS/X,
    --blaslib="-lgoto" for GOTO BLAS
    --blaslib="-lf77blas -lcblas -latlas" for ATLAS
    --blaslib="-lmkl_em64t -lguide" for MKL on emt64 architecture (remember to set environment variable MKL_NUM_THREADS=1)
    --blaslib="-lmkl_intel_lp64 -lmkl_sequential -lmkl_core" for single threaded MKL on 64-bit architectures using Intel compilers
    --blaslib="-lmkl_intel -lmkl_sequential -lmkl_core" for single threaded MKL on 32-bit architectures using Intel compilers
    --blaslib="-lmkl_gf_lp64 -lmkl_sequential -lmkl_core" for single threaded MKL on 32-bit architectures using GNU Fortran compilers
    etc...
.'
The BLAS library is not needed in the case where testing is disabled
by means of the --notesting flag.
                """
                sys.exit()

        # CUSTOM CHECKS
        self.check_cc()
        self.check_fc()
        self.set_mangling()
        self.set_download()
        self.set_ranlib()
        self.detect_compilers()
        self.check_linking()
        if self.testing:
           self.set_nbcores()

        return

    def usage(self):
          print "="*40
          print """
          PLASMA configuration script version %d.%d.%d
          The script will try to figure out some of the features of your system
          and the location of few other tools required for
          the installation of the ScaLAPACK package and the other software
          packages that it requires.
          It is strongly recommended that you help the script by providing info 
          through the flags listed below


          -h or --help        : prints this message

          --prefix            : path where to create the libraries, build and log of the installer

          --cc=[CMD]          : the C compiler.

          --fc=[CMD]          : the Fortran compiler.

          --ccflags=[FLAGS]   : the flags for the C compiler
                                 (default -O3)

          --fcflags=[FLAGS]   : the flags for the Fortran compiler
                                 (default -O3)

          --ldflags_c=[flags] : loader flags when main program is in C. Some compilers (e.g. PGI) require 
                                 different options when linking C main programs to Fortran subroutines
                                 and vice-versa

          --ldflags_fc=[flags]: loader flags when main program is in Fortran. Some compilers (e.g. PGI) require 
                                 different options when linking Fortran main programs to C subroutines
                                 and vice-versa

          --makecmd=[CMD]     : the make command
                                 (make by default)

          --blaslib=[LIB]     : a BLAS library
                                 (path should be absolute if --prefix is used)

          --downblas          : if you do not want to provide a BLAS
                                 we can download and install it for you

          --nbcores           : The number of cores to be used by the testing.
                                 (2 by default)

          --notesting         : disables the PLASMA testing. The BLAS library is not
                                required in this case.

          --clean             : cleans up the installer directory.

          --src_path=PATH     : path to PLASMA source code (this option disables download from
                                PLASMA web site). Typical usage (with path of SVN's trunk):
                                --src_path=/home/user/svn/plasma/trunk
          """ % self.config.version
          print "="*40
          sys.exit()

    def parse_args(self, argv):
        """ Parse input argument to get compilers etc. from command line. """

        try:
          opts, args = getopt.getopt(argv[1:], "h", ["help","prefix=",
          "ccflags=", "fcflags=", "noopt=", "makecmd=", "blaslib=",
          "ldflags_c=", "ldflags_fc=", "cc=", "fc=",
          "downblas", "nbcores=", "notesting", "clean", "src_path="])
        except getopt.error, msg:
          print msg
          print "for help use --help"
          sys.exit(2)
        # process options
        for o, a in opts:
          if o in ("-h", "--help"):
            self.usage()
            sys.exit(0)
          else:
            if o == '--clean':
                self.clean = 1
                return
            elif o == '--prefix':
                self.prefix = fixpaths(a)
                print 'Install directory is...', self.prefix
            elif o == '--ccflags':
                self.config.ccflags = a
                print 'C flags are ', a
            elif o=='--fcflags':
                self.config.fcflags = a
                print 'Fortran flags are ', a
            elif o=='--noopt':
                self.config.noopt = a
                print 'NOOPT flags are ', a
            elif o=='--makecmd':
                self.make = a
            elif o=='--cc':
                self.config.cc = a
                print 'The C compiler is ', a
            elif o=='--fc':
                self.config.fc = a
                print 'The Fortran compiler is ', a
            elif o == '--blaslib':
                if ("-l" in a):
                   self.config.blaslib = a
                else:
                   self.config.blaslib = fixpaths(a)
            elif o == '--downblas':
                self.downblas = 1
            elif o == '--nbcores':
                self.nbcores = a
            elif o == '--notesting':
                self.testing = 0
            elif o == '--ldflags_c':
                self.config.ldflags_c = a
            elif o == '--src_path':
                self.src_path = a

    def check_cc(self):
        """ checks if cc works """
        # simply generates a C program containing a couple of calls
        # to MPI routines and checks if the compilation and execution
        # are succesful
        print 'Checking if cc works...',
        sys.stdout.flush()
        # generate
        writefile('tmpc.c',"""
            #include <stdio.h>
            int main(int argc, char **argv){
            int iam;
            fprintf(stdout, \"success\" );fflush(stdout);
            return 0;
            }\n""")

        # compile
        ccomm = self.config.cc+" "+self.config.ccflags+" -o tmpc "+os.path.join(os.getcwd(),"tmpc.c")
        (output, error, retz) = runShellCommand(ccomm)

        if retz:
            print '\n\nCOMMON: C compiler not working! aborting...'
            print 'stderr:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        # run
        comm = './tmpc'
        (output, error, retz) = runShellCommand(comm)
        if retz:
            print '\n\nCOMMON: cc not working! aborting...'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        # cleanup
        killfiles(['tmpc.c','tmpc'])
        print 'yes'
        return 0;

    def check_fc(self):
        """ check if the Fortran compiler works """
        # simply generates a F77 program and checks if the compilation and execution
        # are succesful
        print "Checking if the Fortran compiler works...",
        sys.stdout.flush()
        # generate
        writefile("tmpf.f","""
      program ftest
      integer i
      print*,'success'
      stop
      end\n""")

        # compile
        fcomm = self.config.fc+' '+self.config.fcflags+' -o tmpf '+'tmpf.f'
        (output, error, retz) = runShellCommand(fcomm)

        if retz:
            print '\n\nCOMMON: the Fortran compiler is not working! aborting...'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        # run
        comm = './tmpf'
        (output, error, retz) = runShellCommand(comm)
        if retz:
            print '\n\nCOMMON: the Fortran compiler is not working! aborting...'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        # cleanup
        killfiles(['tmpf.f','tmpf','tmpf.o'])
        print 'yes'

        return 0;

    def set_mangling(self):
        """ Sets the INTFACE variable in Bmake.inc """
        # This one generates a program equivalent to that in BLACS/INSTALL
        # that checks the mangling in FORTRAN function symbols
        print 'Setting Fortran mangling...',
        sys.stdout.flush()
        writefile('tmpf.f',"""
      program intface
      external c_intface
      integer i
      call c_intface(i)
      stop
      end\n""")
        writefile('tmpc.c',"""
      #include <stdio.h>
      void c_intface_(int *i){fprintf(stdout, \"-DADD_\");fflush(stdout);}
      void c_intface(int *i){fprintf(stdout, \"-DNOCHANGE\");fflush(stdout);}
      void c_intface__(int *i){fprintf(stdout, \"-DfcIsF2C\");fflush(stdout);}
      void C_INTFACE(int *i){fprintf(stdout, \"-DUPCASE\");fflush(stdout);}\n""")

        ccomm = self.config.cc+' '+self.config.ccflags+' -c tmpc.c -o tmpc.o'
        fcomm = self.config.fc+' '+self.config.fcflags+' tmpf.f tmpc.o -o xintface'

        (output, error, retz) = runShellCommand(ccomm)
        if retz:
            print '\n\nCOMMON: in set_mangling: cannot compile'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        (output, error, retz) = runShellCommand(fcomm)
        if retz:
            print '\n\nCOMMON: in set_mangling: cannot compile'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        comm = os.path.join(os.getcwd(),'xintface')
        (output, error, retz) = runShellCommand(comm)
        if retz:
            print '\n\nCOMMON: in set_mangling: cannot run xintface'
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        self.mangling = output
        killfiles(['xintface', 'tmpf.f', 'tmpf.o', 'tmpc.c', 'tmpc.o'])

        print self.mangling
        return 1;


    def check_linking(self):
        """ Check if C main can be linked to Fortran subroutine """

        # This one checks if the linking command works out of the box or
        # if any specific flag is required. For example if the linker if the
        # Intel FORTRAN compiler, then the "-nofor_main" is usually required.
        # This function only checks if linker works but does not automatically
        # detect the required flags
        print 'Checking loader...',
        sys.stdout.flush()
        writefile('tmpf.f',"""
      subroutine fsub()
      write(*,*)'success'
      stop
      end\n""")
        writefile('tmpc.c',"""
      #if defined ADD_
      #define fsub fsub_
      #elif defined NOCHANGE
      #define fsub fsub
      #elif defined fcIsF2C
      #define fsub fsub_
      #elif defined UPCASE
      #define fsub FSUB
      #endif
      void main(){
      fsub();}\n""")

        ccomm = self.config.cc+' '+self.config.ccflags+' '+self.mangling+' -c -o tmpc.o tmpc.c'
        fcomm = self.config.fc+' '+self.config.fcflags+' -c -o tmpf.o tmpf.f'
        lcomm = self.config.fc+' '+self.config.ldflags_c+' -o lnk tmpf.o tmpc.o'

        (output, error, retz) = runShellCommand(ccomm)
        if retz:
            print '\n\nCOMMON: in check_linking: cannot compile'
            print 'command is: ',ccomm
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        (output, error, retz) = runShellCommand(fcomm)
        if retz:
            print '\n\nCOMMON: in check_linking: cannot compile'
            print 'command is: ',fcomm
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()

        (output, error, retz) = runShellCommand(lcomm)
        if retz:
            print """\n\nCOMMON: in check_linking: cannot link
            Cannot link a C main program to a Fortran77 subroutine
            Make sure that the appropriate flags are passed to the linker."""
            print 'command is: ',lcomm
            print 'error is:\n','*'*40,'\n',error,'\n','*'*40
            sys.exit()


        killfiles(['lnk', 'tmpf.f', 'tmpf.o', 'tmpc.c', 'tmpc.o'])

        print 'works'
        return 1;



    def set_download(self):
        """ Figures out how to download files """
        print 'Setting download command...'
        wget = 0
        urllib = 0
        # JULIE : Cut proxy stuff...was causing problems (see scalapack installer if you want it back)
        if urllib == 0:
            # if urllib2 is not present checks if wget is present
            # in the PATH and if yes it sets the download command
            # to be wget
            print "Checking availablility of wget...",
            path=str(os.getenv('PATH')).split(os.pathsep)
            for i in path:
                if (os.path.isfile(os.path.join(i,'wget'))):
                    print "available"
                    wget = 1
                    break
            if wget:
                # test wget
                print "Testing wget...",
                comm = 'wget http://www.netlib.org/lapack/index'
                (output, error, retz) = runShellCommand(comm)
                if(retz != 0):
                    print 'not working.'
                    wget = -1
                else:
                    print "working"
                    self.downcmd="wget"
                    os.remove("index")
                    return
            else:
                # wget not available
                print "not available"
                wget=0


    def set_ranlib(self):
        """ Sets the ranlib command """
        # Some systems don't have the ranlib command (e.g. SGIs). 
        # In the case where ranlib is not present in the PATH,
        # echo is used instead of ranlib
        print "Setting ranlib command...",

        path=str(os.getenv('PATH')).split(os.pathsep)
        for i in path:
            if os.path.isfile(os.path.join(i,'ranlib')):
                self.ranlib=os.path.join(i,'ranlib')
                print self.ranlib
                return

        for i in path:
            if os.path.isfile(os.path.join(i,'echo')):
                self.ranlib=os.path.join(i,'echo')
                print self.ranlib
                return

    def detect_compilers(self):
        """ Tries to detect the compilers type """
        # By users experience it is known which compiler flags are required
        # in some cases. This function tries to detect which compilers are used
        # and sets the flags accordingly

        print 'Detecting Fortran compiler...',
        if self.fc_is_intel():
            # The Intel FORTRAN compiler requires -nofor_main flag
            # for the linking and the -mp flag to maintain the 
            # floating-point precision
            self.config.fcflags    += ' -diag-disable vec -fltconsistency -fp_port'
            self.config.ldflags_c   += ' -nofor_main'
            self.config.ldflags_fc += ''
            self.config.noopt += ' -mp'
            print 'Intel'
        elif self.fc_is_gnu():
            print 'GNU'
        elif self.fc_is_xlf():
            self.config.fcflags    += ' -qstrict -qthreaded'
            print 'IBM'
        elif self.fc_is_pgi():
            self.config.ldflags_c   += ''
            self.config.ldflags_fc += ''
        else:
            print 'unknown'

        print 'Detecting C compiler...',
        if self.cc_is_intel():
            self.config.ccflags    += ' -diag-disable vec'
            print 'Intel'
        elif self.cc_is_gnu():
            print 'GNU'
        elif self.cc_is_xlc():
            self.config.ccflags     += ' -qstrict -qthreaded'
            print 'IBM'
        elif self.cc_is_pgi():
            print 'PGI'
        else:
            print 'unknown'


        print 'Selected C compiler flags: '+self.config.ccflags
        print 'Selected Fortran compiler flags: '+self.config.fcflags
        print 'Selected loader flags (C main): '+self.config.ldflags_c
        print 'Selected loader flags (Fortran main): '+self.config.ldflags_fc
#        print 'Selected NOOPT flags: '+self.config.noopt

        return


    def fc_is_intel(self):
        comm = self.config.fc+' -V'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(error,'Intel(R) Fortran')
        if isifort != -1:
            return 1

        return 0


    def fc_is_gnu(self):
        comm = self.config.fc+' --help'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(output,'gnu.org')
        if isifort != -1:
            return 1

        return 0

    def fc_is_xlf(self):
        comm = self.config.fc
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(output,'xlf')
        if isifort != -1:
            return 1

        return 0
    def fc_is_pgi(self):
        comm = self.config.fc+' -V'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(error,'pgifc')
        if isifort != -1:
            return 1
        isifort = string.find(error,'pgif95')
        if isifort != -1:
            return 1
        isifort = string.find(error,'Portland')
        if isifort != -1:
            return 1
        isifort = string.find(output,'pgifc')
        if isifort != -1:
            return 1
        isifort = string.find(output,'pgif95')
        if isifort != -1:
            return 1
        isifort = string.find(output,'Portland')
        if isifort != -1:
            return 1

        return 0

    def cc_is_intel(self):
        comm = self.config.cc+' -V'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(error,'Intel(R) C')
        if isifort != -1:
            return 1

        return 0


    def cc_is_gnu(self):
        comm = self.config.cc+' --help'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(output,'gnu.org')
        if isifort != -1:
            return 1

        return 0

    def cc_is_xlc(self):
        comm = self.config.cc+' -qversion'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(output,'XL')
        if isifort != -1:
            return 1

        return 0

    def cc_is_pgi(self):
        comm = self.config.cc+' -V'
        (output, error, retz) = runShellCommand(comm)
        isifort = string.find(error,'pgicc')
        if isifort != -1:
            return 1
        isifort = string.find(error,'Portland')
        if isifort != -1:
            return 1
        isifort = string.find(output,'pgicc')
        if isifort != -1:
            return 1
        isifort = string.find(output,'Portland')
        if isifort != -1:
            return 1

        return 0

    def set_nbcores(self):
        print 'Setting environment variable...',
        os.environ['OMP_NUM_THREADS']='1'
        os.environ['GOTO_NUM_THREADS']='1'
        os.environ['MKL_NUM_THREADS']='1'
        print 'done.'
        if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"):
           ncpus = os.sysconf("SC_NPROCESSORS_ONLN")
           print 'Checking number of cores available on the machine: ', ncpus
           if int(ncpus)<int(self.nbcores):
              print '---> INFO: you do not have' , self.nbcores , 'cores on that machine! (will use the default 2)'
              self.nbcores=2
           print 'Number of cores to be used for PLASMA testing: ',  self.nbcores
           if int(self.nbcores)==1:
              print 'WARNING: PLASMA is meant to run on more than one core'

        return 0


    def resume(self):

        cwd = os.getcwd()
        print """
******************************************************
******************************************************

PLASMA installation completed.


Your BLAS library is:\n"""+self.config.blaslib+"""
\nYour CBLAS libraries are:\n"""+self.cblaslib+"""
\nYour CORE_LAPACK library is:\n"""+self.corelapacklib+"""\n
\nYour CORE_BLAS library is:\n"""+self.coreblaslib+"""\n
Your PLASMA library is:\n"""+self.plasmalib+"""

Log messages are in the\n"""+os.path.join(cwd,'log')+"""
directory.

The Plasma testing programs are in:\n"""+os.path.join(cwd,'build/plasma/testing')+"""


The\n"""+os.path.join(cwd,'build')+"""
directory contains the source code of the libraries 
that have been installed. It can be removed at this time.


******************************************************
******************************************************
"""



    def cleanup(self):
        """ Cleans up the installer directory """

        print "Cleaning up...",
        sys.stdout.flush()

        cwd = os.getcwd()
        builddir = os.path.join(cwd,'build')
        libdir   = os.path.join(cwd,'lib')
        logdir   = os.path.join(cwd,'log')

        comm = 'rm -rf '+builddir+' '+libdir+' '+logdir
        (output, error, retz) = runShellCommand(comm)

        print "done."
