001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import java.io.BufferedReader;
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileReader;
035import java.io.FileWriter;
036import java.io.IOException;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044
045import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
046import edu.wisc.ssec.mcidasv.startupmanager.Platform;
047
048public class OptionMaster {
049
050    public final static String SET_PREFIX = "SET ";
051    public final static String EMPTY_STRING = "";
052    public final static String QUOTE_STRING = "\"";
053    public final static char QUOTE_CHAR = '"';
054
055    // TODO(jon): write CollectionHelpers.zip() and CollectionHelpers.zipWith()
056    public final Object[][] blahblah = {
057        { "HEAP_SIZE", "Memory", "512m", Type.MEMORY, OptionPlatform.ALL, Visibility.VISIBLE },
058        { "JOGL_TOGL", "Enable JOGL", "1", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
059        { "USE_3DSTUFF", "Enable 3D controls", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
060        { "DEFAULT_LAYOUT", "Load default layout", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
061        { "STARTUP_BUNDLE", "Defaults", "0;", Type.FILE, OptionPlatform.ALL, Visibility.VISIBLE },
062        // mcidasv enables this (the actual property is "visad.java3d.geometryByRef")
063        // by default in mcidasv.properties.
064        { "USE_GEOBYREF", "Enable access to geometry by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
065        { "USE_IMAGEBYREF", "Enable access to image data by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
066        { "USE_NPOT", "Enable Non-Power of Two (NPOT) textures", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
067        // temp bandaid for people suffering from permgen problems.
068        { "USE_CMSGC", "Enable concurrent mark-sweep garbage collector", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
069        { "LOG_LEVEL", "Log Level", "INFO", Type.LOGLEVEL, OptionPlatform.ALL, Visibility.VISIBLE },
070        { "JVM_OPTIONS", "Java Virtual Machine Options", "", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
071        { "TEXTURE_WIDTH", "Texture Size", "4096", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
072    };
073    
074    /**
075     * {@link Option}s can be either platform-specific or applicable to all
076     * platforms. Options that are platform-specific still appear in the 
077     * UI, but their component is not enabled.
078     */
079    public enum OptionPlatform { ALL, UNIXLIKE, WINDOWS };
080    
081    /**
082     * The different types of {@link Option}s.
083     * 
084     * @see TextOption
085     * @see BooleanOption
086     * @see MemoryOption
087     * @see DirectoryOption
088     * @see SliderOption
089     * @see LoggerLevelOption
090     * @see FileOption
091     */
092    public enum Type { TEXT, BOOLEAN, MEMORY, DIRTREE, SLIDER, LOGLEVEL, FILE };
093    
094    /** 
095     * Different ways that an {@link Option} might be displayed.
096     */
097    public enum Visibility { VISIBLE, HIDDEN };
098    
099    /** Maps an option ID to the corresponding object. */
100    private Map<String, ? extends Option> optionMap;
101    
102    private static OptionMaster instance;
103    
104    public OptionMaster() {
105        normalizeUserDirectory();
106        optionMap = buildOptions(blahblah);
107//        readStartup();
108    }
109    
110    public static OptionMaster getInstance() {
111        if (instance == null) {
112            instance = new OptionMaster();
113        }
114        return instance;
115    }
116    
117    /**
118     * Creates the specified options and returns a mapping of the option ID
119     * to the actual {@link Option} object.
120     * 
121     * @param options An array specifying the {@code Option}s to be built.
122     * 
123     * @return Mapping of ID to {@code Option}.
124     * 
125     * @throws AssertionError if the option array contained an entry that
126     * this method cannot build.
127     */
128    private Map<String, Option> buildOptions(final Object[][] options) {
129        // TODO(jon): seriously, get that zip stuff working! this array 
130        // stuff is BAD.
131        Map<String, Option> optMap = new HashMap<>(options.length);
132        
133        for (Object[] arrayOption : options) {
134            String id = (String)arrayOption[0];
135            String label = (String)arrayOption[1];
136            String defaultValue = (String)arrayOption[2];
137            Type type = (Type)arrayOption[3];
138            OptionPlatform platform = (OptionPlatform)arrayOption[4];
139            Visibility visibility = (Visibility)arrayOption[5];
140            
141            switch (type) {
142                case TEXT:
143                    optMap.put(id, new TextOption(id, label, defaultValue, platform, visibility));
144                    break;
145                case BOOLEAN:
146                    optMap.put(id, new BooleanOption(id, label, defaultValue, platform, visibility));
147                    break;
148                case MEMORY:
149                    optMap.put(id, new MemoryOption(id, label, defaultValue, platform, visibility));
150                    break;
151                case DIRTREE:
152                    optMap.put(id, new DirectoryOption(id, label, defaultValue, platform, visibility));
153                    break;
154                case SLIDER:
155                    optMap.put(id, new SliderOption(id, label, defaultValue, platform, visibility));
156                    break;
157                case LOGLEVEL:
158                    optMap.put(id, new LoggerLevelOption(id, label, defaultValue, platform, visibility));
159                    break;
160                case FILE:
161                    optMap.put(id, new FileOption(id, label, defaultValue, platform, visibility));
162                    break;
163                default:
164                     throw new AssertionError(type + 
165                         " is not known to OptionMaster.buildOptions()");
166            }
167        }
168        return optMap;
169    }
170    
171    /**
172     * Converts a {@link Platform} to its corresponding 
173     * {@link OptionPlatform} type.
174     * 
175     * @return The current platform as a {@code OptionPlatform} type.
176     * 
177     * @throws AssertionError if {@link StartupManager#getPlatform()} 
178     * returned something that this method cannot convert.
179     */
180    // a lame-o hack :(
181    protected OptionPlatform convertToOptionPlatform() {
182        Platform platform = StartupManager.getInstance().getPlatform();
183        switch (platform) {
184            case WINDOWS: 
185                return OptionPlatform.WINDOWS;
186            case UNIXLIKE: 
187                return OptionPlatform.UNIXLIKE;
188            default: 
189                throw new AssertionError("Unknown platform: " + platform);
190        }
191    }
192    
193    /**
194     * Returns the {@link Option} mapped to {@code id}.
195     * 
196     * @param id The ID whose associated {@code Option} is to be returned.
197     * 
198     * @return Either the {@code Option} associated with {@code id}, or 
199     * {@code null} if there was no association.
200     * 
201     * 
202     * @see #getMemoryOption
203     * @see #getBooleanOption
204     * @see #getDirectoryOption
205     * @see #getSliderOption
206     * @see #getTextOption
207     * @see #getLoggerLevelOption
208     * @see #getFileOption
209     */
210    private Option getOption(final String id) {
211        return optionMap.get(id);
212    }
213    
214    /**
215     * Searches {@link #optionMap} for the {@link MemoryOption} that 
216     * corresponds with the given {@code id}.
217     * 
218     * @param id Identifier for the desired {@code MemoryOption}. 
219     * Should not be {@code null}.
220     * 
221     * @return Either the {@code MemoryOption} that corresponds to {@code id} 
222     * or {@code null}.
223     */
224    public MemoryOption getMemoryOption(final String id) {
225        return (MemoryOption)optionMap.get(id);
226    }
227    
228    /**
229     * Searches {@link #optionMap} for the {@link BooleanOption} that 
230     * corresponds with the given {@code id}.
231     * 
232     * @param id Identifier for the desired {@code BooleanOption}. 
233     * Should not be {@code null}.
234     * 
235     * @return Either the {@code BooleanOption} that corresponds to {@code id} 
236     * or {@code null}.
237     */
238    public BooleanOption getBooleanOption(final String id) {
239        return (BooleanOption)optionMap.get(id);
240    }
241    
242    /**
243     * Searches {@link #optionMap} for the {@link DirectoryOption} that 
244     * corresponds with the given {@code id}.
245     * 
246     * @param id Identifier for the desired {@code DirectoryOption}. 
247     * Should not be {@code null}.
248     * 
249     * @return Either the {@code DirectoryOption} that corresponds to 
250     * {@code id} or {@code null}.
251     */
252    public DirectoryOption getDirectoryOption(final String id) {
253        return (DirectoryOption)optionMap.get(id);
254    }
255    
256    /**
257     * Searches {@link #optionMap} for the {@link SliderOption} that 
258     * corresponds with the given {@code id}.
259     * 
260     * @param id Identifier for the desired {@code SliderOption}. 
261     * Should not be {@code null}.
262     * 
263     * @return Either the {@code SliderOption} that corresponds to {@code id} 
264     * or {@code null}.
265     */
266    public SliderOption getSliderOption(final String id) {
267        return (SliderOption)optionMap.get(id);
268    }
269    
270    /**
271     * Searches {@link #optionMap} for the {@link TextOption} that 
272     * corresponds with the given {@code id}.
273     * 
274     * @param id Identifier for the desired {@code TextOption}. 
275     * Should not be {@code null}.
276     * 
277     * @return Either the {@code TextOption} that corresponds to {@code id} 
278     * or {@code null}.
279     */
280    public TextOption getTextOption(final String id) {
281        return (TextOption)optionMap.get(id);
282    }
283    
284    /**
285     * Searches {@link #optionMap} for the {@link LoggerLevelOption} that 
286     * corresponds with the given {@code id}.
287     * 
288     * @param id Identifier for the desired {@code LoggerLevelOption}. 
289     * Should not be {@code null}.
290     * 
291     * @return Either the {@code LoggerLevelOption} that corresponds to {@code id} 
292     * or {@code null}.
293     */
294    public LoggerLevelOption getLoggerLevelOption(final String id) {
295        return (LoggerLevelOption)optionMap.get(id);
296    }
297
298    public FileOption getFileOption(final String id) {
299        return (FileOption)optionMap.get(id);
300    }
301
302    // TODO(jon): getAllOptions and optionsBy* really need some work.
303    // I want to eventually do something like:
304    // Collection<Option> = getOpts().byPlatform(WINDOWS, ALL).byType(BOOLEAN).byVis(HIDDEN)
305    /**
306     * Returns all the available startup manager options.
307     * 
308     * @return Either all available startup manager options or an empty 
309     * {@link Collection}.
310     */
311    public Collection<Option> getAllOptions() {
312        return Collections.unmodifiableCollection(optionMap.values());
313    }
314    
315    /**
316     * Returns the {@link Option Options} applicable to the given 
317     * {@link OptionPlatform OptionPlatforms}.
318     * 
319     * @param platforms Desired platforms. Cannot be {@code null}.
320     * 
321     * @return Either a {@link List} of {code Option}-s applicable to
322     * {@code platforms} or an empty {@code List}.
323     */
324    public List<Option> optionsByPlatform(
325        final Collection<OptionPlatform> platforms) 
326    {
327        if (platforms == null) {
328            throw new NullPointerException("must specify platforms");
329        }
330        Collection<Option> allOptions = getAllOptions();
331        List<Option> filteredOptions = 
332            new ArrayList<>(allOptions.size());
333        for (Option option : allOptions) {
334            if (platforms.contains(option.getOptionPlatform())) {
335                filteredOptions.add(option);
336            }
337        }
338        return filteredOptions;
339    }
340    
341    /**
342     * Returns the {@link Option Options} that match the given 
343     * {@link Type Types}. 
344     * 
345     * @param types Desired {@code Option} types. Cannot be {@code null}.
346     * 
347     * @return Either the {@code List} of {@code Option}-s that match the given 
348     * types or an empty {@code List}.
349     */
350    public List<Option> optionsByType(final Collection<Type> types) {
351        if (types == null) {
352            throw new NullPointerException("must specify types");
353        }
354        Collection<Option> allOptions = getAllOptions();
355        List<Option> filteredOptions = 
356            new ArrayList<>(allOptions.size());
357        for (Option option : allOptions) {
358            if (types.contains(option.getOptionType())) {
359                filteredOptions.add(option);
360            }
361        }
362        return filteredOptions;
363    }
364    
365    /**
366     * Returns the {@link Option Options} that match the given levels of 
367     * {@link Visibility visibility}.
368     * 
369     * @param visibilities Desired visibility levels. Cannot be {@code null}.
370     * 
371     * @return Either the {@code List} of {@code Option}-s that match the given 
372     * visibility levels or an empty {@code List}. 
373     */
374    public List<Option> optionsByVisibility(
375        final Collection<Visibility> visibilities) 
376    {
377        if (visibilities == null) {
378            throw new NullPointerException("must specify visibilities");
379        }
380        Collection<Option> allOptions = getAllOptions();
381        List<Option> filteredOptions = 
382            new ArrayList<>(allOptions.size());
383        for (Option option : allOptions) {
384            if (visibilities.contains(option.getOptionVisibility())) {
385                filteredOptions.add(option);
386            }
387        }
388        return filteredOptions;
389    }
390    
391    private void normalizeUserDirectory() {
392        StartupManager startup = StartupManager.getInstance();
393        Platform platform = startup.getPlatform();
394        File dir = new File(platform.getUserDirectory());
395        File prefs = new File(platform.getUserPrefs());
396        
397        if (!dir.exists()) {
398            dir.mkdir();
399        }
400        if (!prefs.exists()) {
401            try {
402                File defaultPrefs = new File(platform.getDefaultPrefs());
403                startup.copy(defaultPrefs, prefs);
404            } catch (IOException e) {
405                System.err.println("Non-fatal error copying user preference template: "+e.getMessage());
406            }
407        }
408    }
409
410    public void readStartup() {
411        String line;
412        
413        File script = 
414            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
415        System.err.println("reading "+script);
416        if (script.getPath().isEmpty()) {
417            return;
418        }
419        try {
420            BufferedReader br = new BufferedReader(new FileReader(script));
421            while ((line = br.readLine()) != null) {
422                if (line.startsWith("#")) {
423                    continue;
424                }
425
426                int splitAt = line.indexOf('=');
427                if (splitAt >= 0) {
428                    String id = line.substring(0, splitAt).replace(SET_PREFIX, EMPTY_STRING);
429                    Option option = getOption(id);
430                    if (option != null) {
431                        System.err.println("setting '"+id+"' with '"+line+'\'');
432                        option.fromPrefsFormat(line);
433                    } else {
434                        System.err.println("Warning: Unknown ID '"+id+'\'');
435                    }
436                } else {
437                    System.err.println("Warning: Bad line format '"+line+'\'');
438                }
439            }
440            br.close();
441        } catch (IOException e) {
442            System.err.println("Non-fatal error reading the user preferences: "+e.getMessage());
443        }
444    }
445    
446    public void writeStartup() {
447        File script = 
448            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
449        if (script.getPath().isEmpty()) {
450            return;
451        }
452        // TODO(jon): use filters when you've made 'em less stupid
453        String newLine = 
454                StartupManager.getInstance().getPlatform().getNewLine();
455        OptionPlatform currentPlatform = convertToOptionPlatform();
456        StringBuilder contents = new StringBuilder(2048);
457        for (Object[] arrayOption : blahblah) {
458            Option option = getOption((String)arrayOption[0]);
459            OptionPlatform platform = option.getOptionPlatform();
460            if ((platform == OptionPlatform.ALL) || (platform == currentPlatform)) {
461                contents.append(option.toPrefsFormat()).append(newLine);
462            }
463        }
464        
465        try {
466            BufferedWriter out = 
467                new BufferedWriter(new FileWriter(script));
468            out.write(contents.toString());
469            out.close();
470        } catch (IOException e) {
471            e.printStackTrace();
472        }
473    }
474}