//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2017 OpenVPN Technologies, Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU General Public License Version 3
//    as published by the Free Software Foundation.
//
//    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 in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.
//
//  This code is derived from the Apple sample Reachability.m under
//  the following license.
//
// Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
// Inc. ("Apple") in consideration of your agreement to the following
// terms, and your use, installation, modification or redistribution of
// this Apple software constitutes acceptance of these terms.  If you do
// not agree with these terms, please do not use, install, modify or
// redistribute this Apple software.
//
// In consideration of your agreement to abide by the following terms, and
// subject to these terms, Apple grants you a personal, non-exclusive
// license, under Apple's copyrights in this original Apple software (the
// "Apple Software"), to use, reproduce, modify and redistribute the Apple
// Software, with or without modifications, in source and/or binary forms;
// provided that if you redistribute the Apple Software in its entirety and
// without modifications, you must retain this notice and the following
// text and disclaimers in all such redistributions of the Apple Software.
// Neither the name, trademarks, service marks or logos of Apple Inc. may
// be used to endorse or promote products derived from the Apple Software
// without specific prior written permission from Apple.  Except as
// expressly stated in this notice, no other rights or licenses, express or
// implied, are granted by Apple herein, including but not limited to any
// patent rights that may be infringed by your derivative works or by other
// works in which the Apple Software may be incorporated.
//
// The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
// MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
// THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
// OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
//
// IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
// OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
// MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
// AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
// STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Copyright (C) 2013 Apple Inc. All Rights Reserved.

// Wrapper for Apple SCNetworkReachability methods.

#ifndef OPENVPN_APPLECRYPTO_UTIL_REACHABLE_H
#define OPENVPN_APPLECRYPTO_UTIL_REACHABLE_H

#import "TargetConditionals.h"

#include <netinet/in.h>
#include <SystemConfiguration/SCNetworkReachability.h>

#include <string>
#include <sstream>
#include <memory>

#include <openvpn/common/socktypes.hpp>
#include <openvpn/apple/cf/cf.hpp>
#include <openvpn/apple/reach.hpp>

namespace openvpn {
  namespace CF {
    OPENVPN_CF_WRAP(NetworkReachability, network_reachability_cast, SCNetworkReachabilityRef, SCNetworkReachabilityGetTypeID);
  }

  class ReachabilityBase
  {
  public:
    typedef ReachabilityInterface::Status Status;

    enum Type {
      Internet,
      WiFi,
    };

    std::string to_string() const
    {
      return to_string(flags());
    }

    std::string to_string(const SCNetworkReachabilityFlags f) const
    {
      const Status s = vstatus(f);
      const Type t = vtype();

      std::string ret;
      ret += render_type(t);
      ret += ':';
      ret += render_status(s);
      ret += '/';
      ret += render_flags(f);
      return ret;
    }

    Status status() const
    {
      return vstatus(flags());
    }

    SCNetworkReachabilityFlags flags() const
    {
      SCNetworkReachabilityFlags f = 0;
      if (SCNetworkReachabilityGetFlags(reach(), &f) == TRUE)
	return f;
      else
	return 0;
    }

    static std::string render_type(Type type)
    {
      switch (type) {
      case Internet:
	return "Internet";
      case WiFi:
	return "WiFi";
      default:
	return "Type???";
      }
    }

    static std::string render_status(const Status status)
    {
      switch (status) {
      case ReachabilityInterface::NotReachable:
	return "NotReachable";
      case ReachabilityInterface::ReachableViaWiFi:
	return "ReachableViaWiFi";
      case ReachabilityInterface::ReachableViaWWAN:
	return "ReachableViaWWAN";
      default:
	return "ReachableVia???";
      }
    }

    static std::string render_flags(const SCNetworkReachabilityFlags flags)
    {
      std::string ret;
#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR // Mac OS X doesn't define WWAN flags
      if (flags & kSCNetworkReachabilityFlagsIsWWAN)
	ret += 'W';
      else
#endif
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsReachable)
	ret += 'R';
      else
	ret += '-';
      ret += ' ';
      if (flags & kSCNetworkReachabilityFlagsTransientConnection)
	ret += 't';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsConnectionRequired)
	ret += 'c';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic)
	ret += 'C';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsInterventionRequired)
	ret += 'i';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsConnectionOnDemand)
	ret += 'D';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsIsLocalAddress)
	ret += 'l';
      else
	ret += '-';
      if (flags & kSCNetworkReachabilityFlagsIsDirect)
	ret += 'd';
      else
	ret += '-';
      return ret;
    }

    virtual Type vtype() const = 0;
    virtual Status vstatus(const SCNetworkReachabilityFlags flags) const = 0;

    CF::NetworkReachability reach;
  };

  class ReachabilityViaInternet : public ReachabilityBase
  {
  public:
    ReachabilityViaInternet()
    {
      struct sockaddr_in addr;
      bzero(&addr, sizeof(addr));
      addr.sin_len = sizeof(addr);
      addr.sin_family = AF_INET;
      reach.reset(SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (struct sockaddr*)&addr));
    }

    virtual Type vtype() const
    {
      return Internet;
    }

    virtual Status vstatus(const SCNetworkReachabilityFlags flags) const
    {
      return status_from_flags(flags);
    }

    static Status status_from_flags(const SCNetworkReachabilityFlags flags)
    {
      if ((flags & kSCNetworkReachabilityFlagsReachable) == 0)
	{
	  // The target host is not reachable.
	  return ReachabilityInterface::NotReachable;
	}

      Status ret = ReachabilityInterface::NotReachable;

      if ((flags & kSCNetworkReachabilityFlagsConnectionRequired) == 0)
	{
	  // If the target host is reachable and no connection is required then
	  // we'll assume (for now) that you're on Wi-Fi...
	  ret = ReachabilityInterface::ReachableViaWiFi;
	}

#if 0 // don't contaminate result by considering on-demand viability
      if ((((flags & kSCNetworkReachabilityFlagsConnectionOnDemand ) != 0) ||
	   (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) != 0))
	{
	  // ... and the connection is on-demand (or on-traffic) if the
	  //     calling application is using the CFSocketStream or higher APIs...

	  if ((flags & kSCNetworkReachabilityFlagsInterventionRequired) == 0)
	    {
	      // ... and no [user] intervention is needed...
	      ret = ReachabilityInterface::ReachableViaWiFi;
	    }
	}
#endif

#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR // Mac OS X doesn't define WWAN flags
      if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN)
	{
	  // ... but WWAN connections are OK if the calling application
	  // is using the CFNetwork APIs.
	  ret = ReachabilityInterface::ReachableViaWWAN;
	}
#endif

      return ret;
    }
  };

  class ReachabilityViaWiFi : public ReachabilityBase
  {
  public:
    ReachabilityViaWiFi()
    {
      struct sockaddr_in addr;
      bzero(&addr, sizeof(addr));
      addr.sin_len = sizeof(addr);
      addr.sin_family = AF_INET;
      addr.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); // 169.254.0.0.
      reach.reset(SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (struct sockaddr*)&addr));
    }

    virtual Type vtype() const
    {
      return WiFi;
    }

    virtual Status vstatus(const SCNetworkReachabilityFlags flags) const
    {
      return status_from_flags(flags);
    }

    static Status status_from_flags(const SCNetworkReachabilityFlags flags)
    {
      Status ret = ReachabilityInterface::NotReachable;
      if ((flags & kSCNetworkReachabilityFlagsReachable) && (flags & kSCNetworkReachabilityFlagsIsDirect))
	ret = ReachabilityInterface::ReachableViaWiFi;
      return ret;
    }
  };

  class Reachability : public ReachabilityInterface
  {
  public:
    Reachability(const bool enable_internet, const bool enable_wifi)
    {
      if (enable_internet)
	internet.reset(new ReachabilityViaInternet);
      if (enable_wifi)
	wifi.reset(new ReachabilityViaWiFi);
    }

    bool reachableViaWiFi() const {
      if (internet)
	{
	  if (wifi)
	    return internet->status() == ReachableViaWiFi && wifi->status() == ReachableViaWiFi;
	  else
	    return internet->status() == ReachableViaWiFi;
	}
      else
	{
	  if (wifi)
	    return wifi->status() == ReachableViaWiFi;
	  else
	    return false;
	}
    }

    bool reachableViaCellular() const
    {
      if (internet)
	return internet->status() == ReachableViaWWAN;
      else
	return false;
    }

    virtual Status reachable() const
    {
      if (reachableViaWiFi())
	return ReachableViaWiFi;
      else if (reachableViaCellular())
	return ReachableViaWWAN;
      else
	return NotReachable;
    }

    virtual bool reachableVia(const std::string& net_type) const
    {
      if (net_type == "cellular")
	return reachableViaCellular();
      else if (net_type == "wifi")
	return reachableViaWiFi();
      else
	return reachableViaWiFi() || reachableViaCellular();
    }

    virtual std::string to_string() const
    {
      std::string ret;
      if (internet)
	ret += internet->to_string();
      if (internet && wifi)
	ret += ' ';
      if (wifi)
	ret += wifi->to_string();
      return ret;
    }

    std::unique_ptr<ReachabilityViaInternet> internet;
    std::unique_ptr<ReachabilityViaWiFi> wifi;
  };

  class ReachabilityTracker
  {
  public:
    ReachabilityTracker(const bool enable_internet, const bool enable_wifi)
      : reachability(enable_internet, enable_wifi),
	scheduled(false)
    {
    }

    void reachability_tracker_schedule()
    {
      if (!scheduled)
	{
	  if (reachability.internet)
	    schedule(*reachability.internet, internet_callback_static);
	  if (reachability.wifi)
	    schedule(*reachability.wifi, wifi_callback_static);
	  scheduled = true;
	}
    }

    void reachability_tracker_cancel()
    {
      if (scheduled)
	{
	  if (reachability.internet)
	    cancel(*reachability.internet);
	  if (reachability.wifi)
	    cancel(*reachability.wifi);
	  scheduled = false;
	}
    }

    virtual void reachability_tracker_event(const ReachabilityBase& rb, SCNetworkReachabilityFlags flags) = 0;

    virtual ~ReachabilityTracker()
    {
      reachability_tracker_cancel();
    }

  private:
    bool schedule(ReachabilityBase& rb, SCNetworkReachabilityCallBack cb)
    {
      SCNetworkReachabilityContext context = { 0, this, nullptr, nullptr, nullptr };
      if (rb.reach.defined())
	{
	  if (SCNetworkReachabilitySetCallback(rb.reach(),
					       cb,
					       &context) == FALSE)
	    return false;
	  if (SCNetworkReachabilityScheduleWithRunLoop(rb.reach(),
						       CFRunLoopGetCurrent(),
						       kCFRunLoopCommonModes) == FALSE)
	    return false;
	  return true;
	}
      else
	return false;
    }

    void cancel(ReachabilityBase& rb)
    {
      if (rb.reach.defined())
	SCNetworkReachabilityUnscheduleFromRunLoop(rb.reach(), CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    }

    static void internet_callback_static(SCNetworkReachabilityRef target,
					 SCNetworkReachabilityFlags flags,
					 void *info)
    {
      ReachabilityTracker* self = (ReachabilityTracker*)info;
      self->reachability_tracker_event(*self->reachability.internet, flags);
    }

    static void wifi_callback_static(SCNetworkReachabilityRef target,
				     SCNetworkReachabilityFlags flags,
				     void *info)
    {
      ReachabilityTracker* self = (ReachabilityTracker*)info;
      self->reachability_tracker_event(*self->reachability.wifi, flags);
    }

    Reachability reachability;
    bool scheduled;
  };
}

#endif
