// Copyright (c) 2016, 2017, 2018, 2019 Frank Fischer <frank-fischer@shadow-soft.de>
//
// 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/>
//

#![allow(unused_unsafe)]

use crate::{DVector, Real};

use c_str_macro::c_str;
use cplex_sys as cpx;
use cplex_sys::trycpx;

use std;
use std::ffi::CString;
use std::ptr;
use std::result;

use std::os::raw::{c_char, c_double, c_int};

#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    Solver(cpx::CplexError),
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        use Error::*;
        match self {
            Solver(err) => Some(err),
        }
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
        use Error::*;
        match self {
            Solver(err) => err.fmt(fmt),
        }
    }
}

impl From<cpx::CplexError> for Error {
    fn from(err: cpx::CplexError) -> Error {
        Error::Solver(err)
    }
}

pub type Result<T> = result::Result<T, Error>;

pub struct Solver {
    env: *mut cpx::Env,
    net: *mut cpx::Net,
}

impl Drop for Solver {
    fn drop(&mut self) {
        unsafe {
            cpx::NETfreeprob(self.env, &mut self.net);
        }
        unsafe { cpx::closeCPLEX(&mut self.env) };
    }
}

impl Solver {
    pub fn new(nnodes: usize) -> Result<Solver> {
        let mut status: c_int;
        let env;
        let mut net = std::ptr::null_mut();

        unsafe {
            trycpx!({
                let mut status = 0;
                env = cpx::openCPLEX(&mut status);
                status
            });

            #[allow(clippy::never_loop)]
            loop {
                status = if cfg!(debug_assertions) {
                    cpx::setlogfilename(env, c_str!("mcf.cpxlog").as_ptr(), c_str!("w").as_ptr())
                } else {
                    0
                };
                if status != 0 {
                    break;
                }

                net = cpx::NETcreateprob(env, &mut status, c_str!("mcf").as_ptr());
                if status != 0 {
                    break;
                }
                status = cpx::NETaddnodes(env, net, nnodes as c_int, ptr::null(), ptr::null());
                if status != 0 {
                    break;
                }
                status = cpx::NETchgobjsen(env, net, cpx::ObjectiveSense::Minimize.to_c());
                if status != 0 {
                    break;
                }
                break;
            }

            if status != 0 {
                let msg = CString::new(vec![0; cpx::MESSAGE_BUF_SIZE]).unwrap().into_raw();
                cpx::geterrorstring(env, status, msg);
                cpx::NETfreeprob(env, &mut net);
                return Err(cpx::CplexError {
                    code: status,
                    msg: CString::from_raw(msg).to_string_lossy().into_owned(),
                }
                .into());
            }
        }

        Ok(Solver { env, net })
    }

    pub fn num_nodes(&self) -> usize {
        unsafe { cpx::NETgetnumnodes(self.env, self.net) as usize }
    }

    pub fn num_arcs(&self) -> usize {
        unsafe { cpx::NETgetnumarcs(self.env, self.net) as usize }
    }

    pub fn set_balance(&mut self, node: usize, supply: Real) -> Result<()> {
        let n = node as c_int;
        let s = supply as c_double;
        trycpx!(cpx::NETchgsupply(self.env, self.net, 1, &n, &s as *const c_double));
        Ok(())
    }

    pub fn set_objective(&mut self, obj: &DVector) -> Result<()> {
        let inds = (0..obj.len() as c_int).collect::<Vec<_>>();
        trycpx!(cpx::NETchgobj(
            self.env,
            self.net,
            obj.len() as c_int,
            inds.as_ptr(),
            obj.as_ptr()
        ));
        Ok(())
    }

    pub fn add_arc(&mut self, src: usize, snk: usize, cost: Real, cap: Real) -> Result<()> {
        let f = src as c_int;
        let t = snk as c_int;
        let c = cost as c_double;
        let u = cap as c_double;
        let name = CString::new(format!("x{}#{}_{}", self.num_arcs() + 1, f + 1, t + 1)).unwrap();
        let cname = name.as_ptr();
        trycpx!(cpx::NETaddarcs(
            self.env,
            self.net,
            1,
            &f,
            &t,
            ptr::null(),
            &u,
            &c,
            &cname as *const *const c_char
        ));
        Ok(())
    }

    pub fn solve(&mut self) -> Result<()> {
        trycpx!(cpx::NETprimopt(self.env, self.net));
        Ok(())
    }

    pub fn objective(&self) -> Result<Real> {
        let mut objval: c_double = 0.0;
        trycpx!(cpx::NETgetobjval(self.env, self.net, &mut objval as *mut c_double));
        Ok(objval)
    }

    pub fn get_solution(&self) -> Result<DVector> {
        let mut sol = dvec![0.0; self.num_arcs()];
        let mut stat: c_int = 0;
        let mut objval: c_double = 0.0;
        trycpx!(cpx::NETsolution(
            self.env,
            self.net,
            &mut stat as *mut c_int,
            &mut objval as *mut c_double,
            sol.as_mut_ptr(),
            ptr::null_mut(),
            ptr::null_mut(),
            ptr::null_mut()
        ));
        Ok(sol)
    }

    pub fn writelp(&self, filename: &str) -> Result<()> {
        let fname = CString::new(filename).unwrap();
        trycpx!(cpx::NETwriteprob(self.env, self.net, fname.as_ptr(), ptr::null_mut()));
        Ok(())
    }
}
